mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-19 07:29:14 +00:00
Compare commits
58 Commits
react-impo
...
file-input
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c8a077d48 | ||
|
|
a2fbe121c3 | ||
|
|
ab770c566e | ||
|
|
1867603225 | ||
|
|
cf195da424 | ||
|
|
0cb6aa5d12 | ||
|
|
47871c4ac4 | ||
|
|
7e4d4c3c98 | ||
|
|
b5ef3191b7 | ||
|
|
f30481e229 | ||
|
|
ae010c333b | ||
|
|
43d1f9ee7a | ||
|
|
ec17e8736d | ||
|
|
44b27e791e | ||
|
|
02385027db | ||
|
|
b311072d9b | ||
|
|
87ac077b0a | ||
|
|
87837df35c | ||
|
|
5d72bbd162 | ||
|
|
a4fc1c5b44 | ||
|
|
539eaded73 | ||
|
|
51c4274d84 | ||
|
|
93b2e78092 | ||
|
|
402a00dcd3 | ||
|
|
b63368d5f6 | ||
|
|
74c6d3ee36 | ||
|
|
621aa4362b | ||
|
|
c8919ad11f | ||
|
|
fad76dd1a2 | ||
|
|
b2f6499b87 | ||
|
|
9520e850dd | ||
|
|
4ee5271a83 | ||
|
|
d8de7bcc51 | ||
|
|
7ee31be6d6 | ||
|
|
9cb5ba7ac1 | ||
|
|
53d5942879 | ||
|
|
c380368b61 | ||
|
|
e298f7e5f4 | ||
|
|
c743561c25 | ||
|
|
e73e32fb71 | ||
|
|
b09a48bec4 | ||
|
|
aeef986cf5 | ||
|
|
6f08f50639 | ||
|
|
8fc5f598d0 | ||
|
|
a6e6147e7a | ||
|
|
0a7b05f456 | ||
|
|
1383ea3fe8 | ||
|
|
f8c37e0d14 | ||
|
|
b22d4e29d3 | ||
|
|
0f3327e23b | ||
|
|
07fe2c3c4c | ||
|
|
647e05f93b | ||
|
|
e3126e0b2c | ||
|
|
37a41f497b | ||
|
|
5066298948 | ||
|
|
858bfff1f5 | ||
|
|
f4a8dd4663 | ||
|
|
00c5053401 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@ docs/assets/images/sprite.svg
|
|||||||
node_modules
|
node_modules
|
||||||
src/react
|
src/react
|
||||||
cdn
|
cdn
|
||||||
|
web-types.json
|
||||||
@@ -7,6 +7,7 @@ docs/search.json
|
|||||||
src/components/icon/icons
|
src/components/icon/icons
|
||||||
src/react/index.ts
|
src/react/index.ts
|
||||||
node_modules
|
node_modules
|
||||||
|
package.json
|
||||||
package-lock.json
|
package-lock.json
|
||||||
tsconfig.json
|
tsconfig.json
|
||||||
cdn
|
cdn
|
||||||
|
|||||||
@@ -160,6 +160,7 @@
|
|||||||
"unbundles",
|
"unbundles",
|
||||||
"unbundling",
|
"unbundling",
|
||||||
"unicons",
|
"unicons",
|
||||||
|
"unsanitized",
|
||||||
"unsupportive",
|
"unsupportive",
|
||||||
"valpha",
|
"valpha",
|
||||||
"valuenow",
|
"valuenow",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import { customElementJetBrainsPlugin } from 'custom-element-jet-brains-integration';
|
||||||
import { customElementVsCodePlugin } from 'custom-element-vs-code-integration';
|
import { customElementVsCodePlugin } from 'custom-element-vs-code-integration';
|
||||||
import { parse } from 'comment-parser';
|
import { parse } from 'comment-parser';
|
||||||
import { pascalCase } from 'pascal-case';
|
import { pascalCase } from 'pascal-case';
|
||||||
@@ -200,6 +201,15 @@ export default {
|
|||||||
url: `https://shoelace.style/components/${tag.replace('sl-', '')}`
|
url: `https://shoelace.style/components/${tag.replace('sl-', '')}`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
}),
|
||||||
|
customElementJetBrainsPlugin({
|
||||||
|
excludeCss: true,
|
||||||
|
referencesTemplate: (_, tag) => {
|
||||||
|
return {
|
||||||
|
name: 'Documentation',
|
||||||
|
url: `https://shoelace.style/components/${tag.replace('sl-', '')}`
|
||||||
|
};
|
||||||
|
}
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -137,15 +137,17 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<code class="nowrap">{{ prop.name }}</code>
|
<code class="nowrap">{{ prop.name }}</code>
|
||||||
{% if prop.attribute != prop.name %}
|
{% if prop.attribute | length > 0 %}
|
||||||
<br>
|
{% if prop.attribute != prop.name %}
|
||||||
<sl-tooltip content="This attribute is different from its property">
|
<br>
|
||||||
<small>
|
<sl-tooltip content="This attribute is different from its property">
|
||||||
<code class="nowrap">
|
<small>
|
||||||
{{ prop.attribute }}
|
<code class="nowrap">
|
||||||
</code>
|
{{ prop.attribute }}
|
||||||
</small>
|
</code>
|
||||||
</sl-tooltip>
|
</small>
|
||||||
|
</sl-tooltip>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -185,7 +187,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#properties') }}">attributes and properties</a>.</em></p>
|
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#attributes-and-properties') }}">attributes and properties</a>.</em></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# Events #}
|
{# Events #}
|
||||||
@@ -305,7 +307,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#component-parts') }}">customizing CSS parts</a>.</em></p>
|
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/customizing/#css-parts') }}">customizing CSS parts</a>.</em></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# Animations #}
|
{# Animations #}
|
||||||
@@ -329,7 +331,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#animations') }}">customizing animations</a>.</em></p>
|
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/customizing#animations') }}">customizing animations</a>.</em></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# Dependencies #}
|
{# Dependencies #}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
let codeBlockId = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds copy code buttons to code fields. The provided doc should be a document object provided by JSDOM. The same
|
* Adds copy code buttons to code fields. The provided doc should be a document object provided by JSDOM. The same
|
||||||
* document will be returned with the appropriate DOM manipulations.
|
* document will be returned with the appropriate DOM manipulations.
|
||||||
@@ -5,19 +7,14 @@
|
|||||||
module.exports = function (doc) {
|
module.exports = function (doc) {
|
||||||
doc.querySelectorAll('pre > code').forEach(code => {
|
doc.querySelectorAll('pre > code').forEach(code => {
|
||||||
const pre = code.closest('pre');
|
const pre = code.closest('pre');
|
||||||
const button = doc.createElement('button');
|
const button = doc.createElement('sl-copy-button');
|
||||||
button.setAttribute('type', 'button');
|
|
||||||
button.classList.add('copy-code-button');
|
|
||||||
button.setAttribute('aria-label', 'Copy');
|
|
||||||
button.innerHTML = `
|
|
||||||
<svg class="copy-code-button__copy-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-files" viewBox="0 0 16 16" part="svg">
|
|
||||||
<path d="M13 0H6a2 2 0 0 0-2 2 2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2 2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm0 13V4a2 2 0 0 0-2-2H5a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1zM3 4a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4z"></path>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<svg class="copy-code-button__copied-icon" style="display: none;" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check-lg" viewBox="0 0 16 16" part="svg">
|
if (!code.id) {
|
||||||
<path d="M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06.733.733 0 0 1 1.047 0l3.052 3.093 5.4-6.425a.247.247 0 0 1 .02-.022Z"></path>
|
code.id = `code-block-${++codeBlockId}`;
|
||||||
</svg>
|
}
|
||||||
`;
|
|
||||||
|
button.classList.add('copy-code-button');
|
||||||
|
button.setAttribute('from', code.id);
|
||||||
|
|
||||||
pre.append(button);
|
pre.append(button);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -163,32 +163,6 @@
|
|||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
//
|
|
||||||
// Copy code buttons
|
|
||||||
//
|
|
||||||
(() => {
|
|
||||||
document.addEventListener('click', event => {
|
|
||||||
const button = event.target.closest('.copy-code-button');
|
|
||||||
const pre = button?.closest('pre');
|
|
||||||
const code = pre?.querySelector('code');
|
|
||||||
const copyIcon = button?.querySelector('.copy-code-button__copy-icon');
|
|
||||||
const copiedIcon = button?.querySelector('.copy-code-button__copied-icon');
|
|
||||||
|
|
||||||
if (button && code) {
|
|
||||||
navigator.clipboard.writeText(code.innerText);
|
|
||||||
copyIcon.style.display = 'none';
|
|
||||||
copiedIcon.style.display = 'inline';
|
|
||||||
button.classList.add('copy-code-button--copied');
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
copyIcon.style.display = 'inline';
|
|
||||||
copiedIcon.style.display = 'none';
|
|
||||||
button.classList.remove('copy-code-button--copied');
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Smooth links
|
// Smooth links
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -373,4 +373,12 @@
|
|||||||
hide();
|
hide();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// We're using Turbo, so when a user searches for something, visits a result, and presses the back button, the search
|
||||||
|
// UI will still be visible but not interactive. This removes the search UI when Turbo renders a page so they don't
|
||||||
|
// get trapped.
|
||||||
|
window.addEventListener('turbo:render', () => {
|
||||||
|
document.body.classList.remove('search-visible');
|
||||||
|
document.querySelectorAll('.search__overlay, .search__dialog').forEach(el => el.remove());
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -506,46 +506,39 @@ pre .token.italic {
|
|||||||
|
|
||||||
/* Copy code button */
|
/* Copy code button */
|
||||||
.copy-code-button {
|
.copy-code-button {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0.5rem;
|
top: 0;
|
||||||
right: 0.5rem;
|
right: 0;
|
||||||
background: var(--sl-color-neutral-0);
|
white-space: normal;
|
||||||
border-radius: calc(var(--docs-border-radius) * 0.875);
|
|
||||||
border: solid 1px var(--sl-color-neutral-200);
|
|
||||||
color: var(--sl-color-neutral-800);
|
color: var(--sl-color-neutral-800);
|
||||||
text-transform: uppercase;
|
transition: 150ms opacity, 150ms scale;
|
||||||
padding: 0.5rem;
|
|
||||||
margin: 0;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: 100ms opacity, 100ms scale;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-code-button svg {
|
.copy-code-button::part(button) {
|
||||||
width: 1rem;
|
background-color: var(--sl-color-neutral-50);
|
||||||
height: 1rem;
|
border-radius: 0 var(--docs-border-radius) 0 var(--docs-border-radius);
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-code-button::part(button):hover {
|
||||||
|
background-color: color-mix(in oklch, var(--sl-color-neutral-50), var(--sl-color-neutral-1000) 3%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-code-button::part(button):active {
|
||||||
|
background-color: color-mix(in oklch, var(--sl-color-neutral-50), var(--sl-color-neutral-1000) 6%);
|
||||||
}
|
}
|
||||||
|
|
||||||
pre .copy-code-button {
|
pre .copy-code-button {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
scale: 0.9;
|
scale: 0.75;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre:hover .copy-code-button,
|
pre:hover .copy-code-button,
|
||||||
.copy-code-button:focus-visible {
|
.copy-code-button:focus-within {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
scale: 1;
|
scale: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre:hover .copy-code-button:hover,
|
|
||||||
pre:hover .copy-code-button--copied {
|
|
||||||
background: var(--sl-color-neutral-200);
|
|
||||||
border-color: var(--sl-color-neutral-300);
|
|
||||||
color: var(--sl-color-neutral-900);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Callouts */
|
/* Callouts */
|
||||||
.callout {
|
.callout {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -310,6 +310,96 @@ const App = () => (
|
|||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Submenus
|
||||||
|
|
||||||
|
To create a submenu, nest an `<sl-menu slot="submenu">` element in a [menu item](/components/menu-item).
|
||||||
|
|
||||||
|
```html:preview
|
||||||
|
<sl-dropdown>
|
||||||
|
<sl-button slot="trigger" caret>Edit</sl-button>
|
||||||
|
|
||||||
|
<sl-menu style="max-width: 200px;">
|
||||||
|
<sl-menu-item value="undo">Undo</sl-menu-item>
|
||||||
|
<sl-menu-item value="redo">Redo</sl-menu-item>
|
||||||
|
<sl-divider></sl-divider>
|
||||||
|
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||||
|
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||||
|
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||||
|
<sl-divider></sl-divider>
|
||||||
|
<sl-menu-item>
|
||||||
|
Find
|
||||||
|
<sl-menu slot="submenu">
|
||||||
|
<sl-menu-item value="find">Find…</sl-menu-item>
|
||||||
|
<sl-menu-item value="find-previous">Find Next</sl-menu-item>
|
||||||
|
<sl-menu-item value="find-next">Find Previous</sl-menu-item>
|
||||||
|
</sl-menu>
|
||||||
|
</sl-menu-item>
|
||||||
|
<sl-menu-item>
|
||||||
|
Transformations
|
||||||
|
<sl-menu slot="submenu">
|
||||||
|
<sl-menu-item value="uppercase">Make uppercase</sl-menu-item>
|
||||||
|
<sl-menu-item value="lowercase">Make lowercase</sl-menu-item>
|
||||||
|
<sl-menu-item value="capitalize">Capitalize</sl-menu-item>
|
||||||
|
</sl-menu>
|
||||||
|
</sl-menu-item>
|
||||||
|
</sl-menu>
|
||||||
|
</sl-dropdown>
|
||||||
|
```
|
||||||
|
|
||||||
|
```jsx:react
|
||||||
|
import SlButton from '@shoelace-style/shoelace/dist/react/button';
|
||||||
|
import SlDivider from '@shoelace-style/shoelace/dist/react/divider';
|
||||||
|
import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown';
|
||||||
|
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
|
||||||
|
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
|
||||||
|
|
||||||
|
const css = `
|
||||||
|
.dropdown-hoist {
|
||||||
|
border: solid 2px var(--sl-panel-border-color);
|
||||||
|
padding: var(--sl-spacing-medium);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const App = () => (
|
||||||
|
<>
|
||||||
|
<SlDropdown>
|
||||||
|
<SlButton slot="trigger" caret>Edit</SlButton>
|
||||||
|
|
||||||
|
<SlMenu style="max-width: 200px;">
|
||||||
|
<SlMenuItem value="undo">Undo</SlMenuItem>
|
||||||
|
<SlMenuItem value="redo">Redo</SlMenuItem>
|
||||||
|
<SlDivider />
|
||||||
|
<SlMenuItem value="cut">Cut</SlMenuItem>
|
||||||
|
<SlMenuItem value="copy">Copy</SlMenuItem>
|
||||||
|
<SlMenuItem value="paste">Paste</SlMenuItem>
|
||||||
|
<SlDivider />
|
||||||
|
<SlMenuItem>
|
||||||
|
Find
|
||||||
|
<SlMenu slot="submenu">
|
||||||
|
<SlMenuItem value="find">Find…</SlMenuItem>
|
||||||
|
<SlMenuItem value="find-previous">Find Next</SlMenuItem>
|
||||||
|
<SlMenuItem value="find-next">Find Previous</SlMenuItem>
|
||||||
|
</SlMenu>
|
||||||
|
</SlMenuItem>
|
||||||
|
<SlMenuItem>
|
||||||
|
Transformations
|
||||||
|
<SlMenu slot="submenu">
|
||||||
|
<SlMenuItem value="uppercase">Make uppercase</SlMenuItem>
|
||||||
|
<SlMenuItem value="lowercase">Make lowercase</SlMenuItem>
|
||||||
|
<SlMenuItem value="capitalize">Capitalize</SlMenuItem>
|
||||||
|
</SlMenu>
|
||||||
|
</SlMenuItem>
|
||||||
|
</SlMenu>
|
||||||
|
</SlDropdown>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
:::warning
|
||||||
|
As a UX best practice, avoid using more than one level of submenu when possible.
|
||||||
|
:::
|
||||||
|
|
||||||
### Hoisting
|
### Hoisting
|
||||||
|
|
||||||
Dropdown panels will be clipped if they're inside a container that has `overflow: auto|hidden`. The `hoist` attribute forces the panel to use a fixed positioning strategy, allowing it to break out of the container. In this case, the panel will be positioned relative to its [containing block](https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#Identifying_the_containing_block), which is usually the viewport unless an ancestor uses a `transform`, `perspective`, or `filter`. [Refer to this page](https://developer.mozilla.org/en-US/docs/Web/CSS/position#fixed) for more details.
|
Dropdown panels will be clipped if they're inside a container that has `overflow: auto|hidden`. The `hoist` attribute forces the panel to use a fixed positioning strategy, allowing it to break out of the container. In this case, the panel will be positioned relative to its [containing block](https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#Identifying_the_containing_block), which is usually the viewport unless an ancestor uses a `transform`, `perspective`, or `filter`. [Refer to this page](https://developer.mozilla.org/en-US/docs/Web/CSS/position#fixed) for more details.
|
||||||
@@ -349,7 +439,6 @@ Dropdown panels will be clipped if they're inside a container that has `overflow
|
|||||||
import SlButton from '@shoelace-style/shoelace/dist/react/button';
|
import SlButton from '@shoelace-style/shoelace/dist/react/button';
|
||||||
import SlDivider from '@shoelace-style/shoelace/dist/react/divider';
|
import SlDivider from '@shoelace-style/shoelace/dist/react/divider';
|
||||||
import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown';
|
import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown';
|
||||||
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
|
|
||||||
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
|
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
|
||||||
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
|
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
|
||||||
|
|
||||||
|
|||||||
42
docs/pages/components/file-input.md
Normal file
42
docs/pages/components/file-input.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
meta:
|
||||||
|
title: File Input
|
||||||
|
description: A description of the component goes here.
|
||||||
|
layout: component
|
||||||
|
---
|
||||||
|
|
||||||
|
```html:preview
|
||||||
|
<form id="upload-form">
|
||||||
|
<sl-file-input label="Upload a file" help-text="Select some files" name="myfiles" multiple></sl-file-input>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<sl-button variant="primary" type="submit">Submit</sl-button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const form = document.getElementById('upload-form');
|
||||||
|
|
||||||
|
form.addEventListener('submit', event => {
|
||||||
|
const formData = new FormData(form);
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
for (const file of formData.values()) {
|
||||||
|
console.log(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### First Example
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
### Second Example
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
[component-metadata:sl-file-input]
|
||||||
@@ -44,3 +44,112 @@ const App = () => (
|
|||||||
:::tip
|
:::tip
|
||||||
Menus are intended for system menus (dropdown menus, select menus, context menus, etc.). They should not be mistaken for navigation menus which serve a different purpose and have a different semantic meaning. If you're building navigation, use `<nav>` and `<a>` elements instead.
|
Menus are intended for system menus (dropdown menus, select menus, context menus, etc.). They should not be mistaken for navigation menus which serve a different purpose and have a different semantic meaning. If you're building navigation, use `<nav>` and `<a>` elements instead.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### In Dropdowns
|
||||||
|
|
||||||
|
Menus work really well when used inside [dropdowns](/components/dropdown).
|
||||||
|
|
||||||
|
```html:preview
|
||||||
|
<sl-dropdown>
|
||||||
|
<sl-button slot="trigger" caret>Edit</sl-button>
|
||||||
|
<sl-menu>
|
||||||
|
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||||
|
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||||
|
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||||
|
</sl-menu>
|
||||||
|
</sl-dropdown>
|
||||||
|
```
|
||||||
|
|
||||||
|
```jsx:react
|
||||||
|
import SlButton from '@shoelace-style/shoelace/dist/react/button';
|
||||||
|
import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown';
|
||||||
|
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
|
||||||
|
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
|
||||||
|
|
||||||
|
const App = () => (
|
||||||
|
<SlDropdown>
|
||||||
|
<SlButton slot="trigger" caret>Edit</SlButton>
|
||||||
|
<SlMenu>
|
||||||
|
<SlMenuItem value="cut">Cut</SlMenuItem>
|
||||||
|
<SlMenuItem value="copy">Copy</SlMenuItem>
|
||||||
|
<SlMenuItem value="paste">Paste</SlMenuItem>
|
||||||
|
</SlMenu>
|
||||||
|
</SlDropdown>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Submenus
|
||||||
|
|
||||||
|
To create a submenu, nest an `<sl-menu slot="submenu">` in any [menu item](/components/menu-item).
|
||||||
|
|
||||||
|
```html:preview
|
||||||
|
<sl-menu style="max-width: 200px;">
|
||||||
|
<sl-menu-item value="undo">Undo</sl-menu-item>
|
||||||
|
<sl-menu-item value="redo">Redo</sl-menu-item>
|
||||||
|
<sl-divider></sl-divider>
|
||||||
|
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||||
|
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||||
|
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||||
|
<sl-divider></sl-divider>
|
||||||
|
<sl-menu-item>
|
||||||
|
Find
|
||||||
|
<sl-menu slot="submenu">
|
||||||
|
<sl-menu-item value="find">Find…</sl-menu-item>
|
||||||
|
<sl-menu-item value="find-previous">Find Next</sl-menu-item>
|
||||||
|
<sl-menu-item value="find-next">Find Previous</sl-menu-item>
|
||||||
|
</sl-menu>
|
||||||
|
</sl-menu-item>
|
||||||
|
<sl-menu-item>
|
||||||
|
Transformations
|
||||||
|
<sl-menu slot="submenu">
|
||||||
|
<sl-menu-item value="uppercase">Make uppercase</sl-menu-item>
|
||||||
|
<sl-menu-item value="lowercase">Make lowercase</sl-menu-item>
|
||||||
|
<sl-menu-item value="capitalize">Capitalize</sl-menu-item>
|
||||||
|
</sl-menu>
|
||||||
|
</sl-menu-item>
|
||||||
|
</sl-menu>
|
||||||
|
```
|
||||||
|
|
||||||
|
{% raw %}
|
||||||
|
|
||||||
|
```jsx:react
|
||||||
|
import SlDivider from '@shoelace-style/shoelace/dist/react/divider';
|
||||||
|
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
|
||||||
|
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
|
||||||
|
|
||||||
|
const App = () => (
|
||||||
|
<SlMenu style={{ maxWidth: '200px' }}>
|
||||||
|
<SlMenuItem value="undo">Undo</SlMenuItem>
|
||||||
|
<SlMenuItem value="redo">Redo</SlMenuItem>
|
||||||
|
<SlDivider />
|
||||||
|
<SlMenuItem value="cut">Cut</SlMenuItem>
|
||||||
|
<SlMenuItem value="copy">Copy</SlMenuItem>
|
||||||
|
<SlMenuItem value="paste">Paste</SlMenuItem>
|
||||||
|
<SlDivider />
|
||||||
|
<SlMenuItem>
|
||||||
|
Find
|
||||||
|
<SlMenu slot="submenu">
|
||||||
|
<SlMenuItem value="find">Find…</SlMenuItem>
|
||||||
|
<SlMenuItem value="find-previous">Find Next</SlMenuItem>
|
||||||
|
<SlMenuItem value="find-next">Find Previous</SlMenuItem>
|
||||||
|
</SlMenu>
|
||||||
|
</SlMenuItem>
|
||||||
|
<SlMenuItem>
|
||||||
|
Transformations
|
||||||
|
<SlMenu slot="submenu">
|
||||||
|
<SlMenuItem value="uppercase">Make uppercase</SlMenuItem>
|
||||||
|
<SlMenuItem value="lowercase">Make lowercase</SlMenuItem>
|
||||||
|
<SlMenuItem value="capitalize">Capitalize</SlMenuItem>
|
||||||
|
</SlMenu>
|
||||||
|
</SlMenuItem>
|
||||||
|
</SlMenu>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
:::warning
|
||||||
|
As a UX best practice, avoid using more than one level of submenus when possible.
|
||||||
|
:::
|
||||||
|
|
||||||
|
{% endraw %}
|
||||||
|
|||||||
@@ -250,7 +250,9 @@ Note that multi-select options may wrap, causing the control to expand verticall
|
|||||||
|
|
||||||
### Setting Initial Values
|
### Setting Initial Values
|
||||||
|
|
||||||
Use the `value` attribute to set the initial selection. When using `multiple`, use space-delimited values to select more than one option.
|
Use the `value` attribute to set the initial selection.
|
||||||
|
|
||||||
|
When using `multiple`, the `value` _attribute_ uses space-delimited values to select more than one option. Because of this, `<sl-option>` values cannot contain spaces. If you're accessing the `value` _property_ through Javascript, it will be an array.
|
||||||
|
|
||||||
```html:preview
|
```html:preview
|
||||||
<sl-select value="option-1 option-2" multiple clearable>
|
<sl-select value="option-1 option-2" multiple clearable>
|
||||||
@@ -381,10 +383,8 @@ The preferred placement of the select's listbox can be set with the `placement`
|
|||||||
```
|
```
|
||||||
|
|
||||||
```jsx:react
|
```jsx:react
|
||||||
import {
|
import SlOption from '@shoelace-style/shoelace/dist/react/option';
|
||||||
SlOption,
|
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
|
||||||
SlSelect
|
|
||||||
} from '@shoelace-style/shoelace/dist/react';
|
|
||||||
|
|
||||||
const App = () => (
|
const App = () => (
|
||||||
<SlSelect placement="top">
|
<SlSelect placement="top">
|
||||||
@@ -452,3 +452,53 @@ const App = () => (
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Custom Tags
|
||||||
|
|
||||||
|
When multiple options can be selected, you can provide custom tags by passing a function to the `getTag` property. Your function can return a string of HTML, a <a href="https://lit.dev/docs/templates/overview/">Lit Template</a>, or an [`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement). The `getTag()` function will be called for each option. The first argument is an `<sl-option>` element and the second argument is the tag's index (its position in the tag list).
|
||||||
|
|
||||||
|
Remember that custom tags are rendered in a shadow root. To style them, you can use the `style` attribute in your template or you can add your own [parts](/getting-started/customizing/#css-parts) and target them with the [`::part()`](https://developer.mozilla.org/en-US/docs/Web/CSS/::part) selector.
|
||||||
|
|
||||||
|
```html:preview
|
||||||
|
<sl-select
|
||||||
|
placeholder="Select one"
|
||||||
|
value="email phone"
|
||||||
|
multiple
|
||||||
|
clearable
|
||||||
|
class="custom-tag"
|
||||||
|
>
|
||||||
|
<sl-option value="email">
|
||||||
|
<sl-icon slot="prefix" name="envelope"></sl-icon>
|
||||||
|
Email
|
||||||
|
</sl-option>
|
||||||
|
<sl-option value="phone">
|
||||||
|
<sl-icon slot="prefix" name="telephone"></sl-icon>
|
||||||
|
Phone
|
||||||
|
</sl-option>
|
||||||
|
<sl-option value="chat">
|
||||||
|
<sl-icon slot="prefix" name="chat-dots"></sl-icon>
|
||||||
|
Chat
|
||||||
|
</sl-option>
|
||||||
|
</sl-select>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
const select = document.querySelector('.custom-tag');
|
||||||
|
|
||||||
|
select.getTag = (option, index) => {
|
||||||
|
// Use the same icon used in the <sl-option>
|
||||||
|
const name = option.querySelector('sl-icon[slot="prefix"]').name;
|
||||||
|
|
||||||
|
// You can return a string, a Lit Template, or an HTMLElement here
|
||||||
|
return `
|
||||||
|
<sl-tag removable>
|
||||||
|
<sl-icon name="${name}" style="padding-inline-end: .5rem;"></sl-icon>
|
||||||
|
${option.getTextLabel()}
|
||||||
|
</sl-tag>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
:::warning
|
||||||
|
Be sure you trust the content you are outputting! Passing unsanitized user input to `getTag()` can result in XSS vulnerabilities.
|
||||||
|
:::
|
||||||
|
|||||||
@@ -462,7 +462,7 @@ To disable the browser's error messages, you need to cancel the `sl-invalid` eve
|
|||||||
<sl-button type="reset" variant="default">Reset</sl-button>
|
<sl-button type="reset" variant="default">Reset</sl-button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<script>
|
<script type="module">
|
||||||
const form = document.querySelector('.inline-validation');
|
const form = document.querySelector('.inline-validation');
|
||||||
const nameError = document.querySelector('#name-error');
|
const nameError = document.querySelector('#name-error');
|
||||||
|
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ import '@shoelace-style/shoelace/%NPMDIR%/components/rating/rating.js';
|
|||||||
import { setBasePath } from '@shoelace-style/shoelace/%NPMDIR%/utilities/base-path.js';
|
import { setBasePath } from '@shoelace-style/shoelace/%NPMDIR%/utilities/base-path.js';
|
||||||
|
|
||||||
// Set the base path to the folder you copied Shoelace's assets to
|
// Set the base path to the folder you copied Shoelace's assets to
|
||||||
setBasePath('/path/to/shoelace/%NPMDIR%
|
setBasePath('/path/to/shoelace/%NPMDIR%');
|
||||||
|
|
||||||
// <sl-button>, <sl-icon>, <sl-input>, and <sl-rating> are ready to use!
|
// <sl-button>, <sl-icon>, <sl-input>, and <sl-rating> are ready to use!
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -210,6 +210,12 @@ Shoelace ships with a file called `vscode.html-custom-data.json` that can be use
|
|||||||
|
|
||||||
If `settings.json` already exists, simply add the above line to the root of the object. Note that you may need to restart VS Code for the changes to take affect.
|
If `settings.json` already exists, simply add the above line to the root of the object. Note that you may need to restart VS Code for the changes to take affect.
|
||||||
|
|
||||||
|
## JetBrains IDEs
|
||||||
|
|
||||||
|
If you are using a [JetBrains IDE](https://www.jetbrains.com/) and you are installing Shoelace from NPM, the editor will automatically detect the `web-types.json` file from the package and you should immediately see component information in your editor.
|
||||||
|
|
||||||
|
If you are installing from the CDN, you can [download a local copy](https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace/cdn/web-types.json) and add it to the root of your project.
|
||||||
|
|
||||||
### Other Editors
|
### Other Editors
|
||||||
|
|
||||||
Most popular editors support custom code completion with a bit of configuration. Please [submit a feature request](https://github.com/shoelace-style/shoelace/issues/new/choose) for your editor of choice. PRs are also welcome!
|
Most popular editors support custom code completion with a bit of configuration. Please [submit a feature request](https://github.com/shoelace-style/shoelace/issues/new/choose) for your editor of choice. PRs are also welcome!
|
||||||
|
|||||||
@@ -14,7 +14,25 @@ New versions of Shoelace are released as-needed and generally occur when a criti
|
|||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
- Added the `<sl-copy-button>` component [#1473]
|
- Fixed a bug in `<sl-switch>` that resulted in improper spacing between the label and the required asterisk [#1540]
|
||||||
|
- Updated `@ctrl/tinycolor` to 4.0.1 [#1542]
|
||||||
|
|
||||||
|
## 2.8.0
|
||||||
|
|
||||||
|
- Added `--isolatedModules` and `--verbatimModuleSyntax` to `tsconfig.json`. For anyone directly importing event types, they no longer provide a default export due to these options being enabled. For people using the `events/event.js` file directly, there is no change.
|
||||||
|
- Added support for submenus in `<sl-menu-item>` [#1410]
|
||||||
|
- Added the `--submenu-offset` custom property to `<sl-menu-item>` [#1410]
|
||||||
|
- Fixed an issue with focus trapping elements like `<sl-dialog>` when wrapped by other elements not checking the assigned elements of `<slot>`s. [#1537]
|
||||||
|
- Fixed type issues with the `ref` attribute in React Wrappers. [#1526]
|
||||||
|
- Fixed a regression that caused `<sl-radio-button>` to render incorrectly with gaps [#1523]
|
||||||
|
- Improved expand/collapse behavior of `<sl-tree>` to work more like users expect [#1521]
|
||||||
|
- Improved `<sl-menu-item>` so labels truncate properly instead of getting chopped and overflowing
|
||||||
|
- Removed the extra `React.Component` around `@lit-labs/react` wrapper. [#1531]
|
||||||
|
- Updated `@lit-labs/react` to v2.0.1. [#1531]
|
||||||
|
|
||||||
|
## 2.7.0
|
||||||
|
|
||||||
|
- Added the experimental `<sl-copy-button>` component [#1473]
|
||||||
- Fixed a bug in `<sl-dropdown>` where pressing [[Up]] or [[Down]] when focused on the trigger wouldn't focus the first/last menu items [#1472]
|
- Fixed a bug in `<sl-dropdown>` where pressing [[Up]] or [[Down]] when focused on the trigger wouldn't focus the first/last menu items [#1472]
|
||||||
- Fixed a bug that caused key presses in text fields to be hijacked when used inside `<sl-tree>` [#1492]
|
- Fixed a bug that caused key presses in text fields to be hijacked when used inside `<sl-tree>` [#1492]
|
||||||
- Fixed an upstream bug that caused React CodePen examples to stop working
|
- Fixed an upstream bug that caused React CodePen examples to stop working
|
||||||
|
|||||||
77
package-lock.json
generated
77
package-lock.json
generated
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "@shoelace-style/shoelace",
|
"name": "@shoelace-style/shoelace",
|
||||||
"version": "2.6.0",
|
"version": "2.8.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@shoelace-style/shoelace",
|
"name": "@shoelace-style/shoelace",
|
||||||
"version": "2.6.0",
|
"version": "2.8.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ctrl/tinycolor": "^3.5.0",
|
"@ctrl/tinycolor": "^4.0.1",
|
||||||
"@floating-ui/dom": "^1.2.1",
|
"@floating-ui/dom": "^1.2.1",
|
||||||
"@lit-labs/react": "^1.1.1",
|
"@lit-labs/react": "^2.0.1",
|
||||||
"@shoelace-style/animations": "^1.1.0",
|
"@shoelace-style/animations": "^1.1.0",
|
||||||
"@shoelace-style/localize": "^3.1.1",
|
"@shoelace-style/localize": "^3.1.1",
|
||||||
"composed-offset-position": "^0.0.4",
|
"composed-offset-position": "^0.0.4",
|
||||||
@@ -37,6 +37,7 @@
|
|||||||
"command-line-args": "^5.2.1",
|
"command-line-args": "^5.2.1",
|
||||||
"comment-parser": "^1.3.1",
|
"comment-parser": "^1.3.1",
|
||||||
"cspell": "^6.18.1",
|
"cspell": "^6.18.1",
|
||||||
|
"custom-element-jet-brains-integration": "^1.1.0",
|
||||||
"custom-element-vs-code-integration": "^1.1.0",
|
"custom-element-vs-code-integration": "^1.1.0",
|
||||||
"del": "^7.0.0",
|
"del": "^7.0.0",
|
||||||
"download": "^8.0.0",
|
"download": "^8.0.0",
|
||||||
@@ -832,11 +833,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ctrl/tinycolor": {
|
"node_modules/@ctrl/tinycolor": {
|
||||||
"version": "3.5.0",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.0.1.tgz",
|
||||||
"integrity": "sha512-tlJpwF40DEQcfR/QF+wNMVyGMaO9FQp6Z1Wahj4Gk3CJQYHwA2xVG7iKDFdW6zuxZY9XWOpGcfNCTsX4McOsOg==",
|
"integrity": "sha512-dfimuE1mfaqL8P8jyQzdk9yFeFUWCyhjK5VyydXgDtQO0fezr6aWaGauHnlI07BZBIF45gahb0oxJjkUcylDwQ==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@custom-elements-manifest/analyzer": {
|
"node_modules/@custom-elements-manifest/analyzer": {
|
||||||
@@ -1473,9 +1474,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@lit-labs/react": {
|
"node_modules/@lit-labs/react": {
|
||||||
"version": "1.1.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@lit-labs/react/-/react-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@lit-labs/react/-/react-2.0.1.tgz",
|
||||||
"integrity": "sha512-9TC+/ZWb6BJlWCyUr14FKFlaGnyKpeEDorufXozQgke/VoVrslUQNaL7nBmrAWdNrmzx5jWgi8lFmWwrxMjnlA=="
|
"integrity": "sha512-Nj+XB3HamqaWefN91lpFPJaqjJ78XzGkPWCedB4jyH22GBFEenpE9A/h8B/2dnIGXtNtd9D/RFpUdQ/dBtWFqA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "17 || 18"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@lit-labs/ssr-dom-shim": {
|
"node_modules/@lit-labs/ssr-dom-shim": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
@@ -2269,8 +2273,7 @@
|
|||||||
"node_modules/@types/prop-types": {
|
"node_modules/@types/prop-types": {
|
||||||
"version": "15.7.4",
|
"version": "15.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
|
||||||
"integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==",
|
"integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/qs": {
|
"node_modules/@types/qs": {
|
||||||
"version": "6.9.7",
|
"version": "6.9.7",
|
||||||
@@ -2288,7 +2291,6 @@
|
|||||||
"version": "18.0.26",
|
"version": "18.0.26",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz",
|
||||||
"integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==",
|
"integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"@types/scheduler": "*",
|
"@types/scheduler": "*",
|
||||||
@@ -2316,8 +2318,7 @@
|
|||||||
"node_modules/@types/scheduler": {
|
"node_modules/@types/scheduler": {
|
||||||
"version": "0.16.2",
|
"version": "0.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
|
||||||
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
|
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/semver": {
|
"node_modules/@types/semver": {
|
||||||
"version": "7.5.0",
|
"version": "7.5.0",
|
||||||
@@ -5684,8 +5685,16 @@
|
|||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.0.10",
|
"version": "3.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz",
|
||||||
"integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==",
|
"integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA=="
|
||||||
"dev": true
|
},
|
||||||
|
"node_modules/custom-element-jet-brains-integration": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/custom-element-jet-brains-integration/-/custom-element-jet-brains-integration-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-wesa4OEvRQdxNzynk5ugU7ZRy0Ghkoaa6NmRGTqOASIng1hVaE3EKKO3rK11b4Y/pR3HUPIPKs1mRSnRCjHBfg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"prettier": "^2.8.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/custom-element-vs-code-integration": {
|
"node_modules/custom-element-vs-code-integration": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
@@ -17904,9 +17913,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@ctrl/tinycolor": {
|
"@ctrl/tinycolor": {
|
||||||
"version": "3.5.0",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.0.1.tgz",
|
||||||
"integrity": "sha512-tlJpwF40DEQcfR/QF+wNMVyGMaO9FQp6Z1Wahj4Gk3CJQYHwA2xVG7iKDFdW6zuxZY9XWOpGcfNCTsX4McOsOg=="
|
"integrity": "sha512-dfimuE1mfaqL8P8jyQzdk9yFeFUWCyhjK5VyydXgDtQO0fezr6aWaGauHnlI07BZBIF45gahb0oxJjkUcylDwQ=="
|
||||||
},
|
},
|
||||||
"@custom-elements-manifest/analyzer": {
|
"@custom-elements-manifest/analyzer": {
|
||||||
"version": "0.8.3",
|
"version": "0.8.3",
|
||||||
@@ -18281,9 +18290,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@lit-labs/react": {
|
"@lit-labs/react": {
|
||||||
"version": "1.1.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@lit-labs/react/-/react-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@lit-labs/react/-/react-2.0.1.tgz",
|
||||||
"integrity": "sha512-9TC+/ZWb6BJlWCyUr14FKFlaGnyKpeEDorufXozQgke/VoVrslUQNaL7nBmrAWdNrmzx5jWgi8lFmWwrxMjnlA=="
|
"integrity": "sha512-Nj+XB3HamqaWefN91lpFPJaqjJ78XzGkPWCedB4jyH22GBFEenpE9A/h8B/2dnIGXtNtd9D/RFpUdQ/dBtWFqA==",
|
||||||
|
"requires": {}
|
||||||
},
|
},
|
||||||
"@lit-labs/ssr-dom-shim": {
|
"@lit-labs/ssr-dom-shim": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
@@ -18988,8 +18998,7 @@
|
|||||||
"@types/prop-types": {
|
"@types/prop-types": {
|
||||||
"version": "15.7.4",
|
"version": "15.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz",
|
||||||
"integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==",
|
"integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"@types/qs": {
|
"@types/qs": {
|
||||||
"version": "6.9.7",
|
"version": "6.9.7",
|
||||||
@@ -19007,7 +19016,6 @@
|
|||||||
"version": "18.0.26",
|
"version": "18.0.26",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.26.tgz",
|
||||||
"integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==",
|
"integrity": "sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"@types/scheduler": "*",
|
"@types/scheduler": "*",
|
||||||
@@ -19035,8 +19043,7 @@
|
|||||||
"@types/scheduler": {
|
"@types/scheduler": {
|
||||||
"version": "0.16.2",
|
"version": "0.16.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
|
||||||
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==",
|
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"@types/semver": {
|
"@types/semver": {
|
||||||
"version": "7.5.0",
|
"version": "7.5.0",
|
||||||
@@ -21504,8 +21511,16 @@
|
|||||||
"csstype": {
|
"csstype": {
|
||||||
"version": "3.0.10",
|
"version": "3.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz",
|
||||||
"integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==",
|
"integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA=="
|
||||||
"dev": true
|
},
|
||||||
|
"custom-element-jet-brains-integration": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/custom-element-jet-brains-integration/-/custom-element-jet-brains-integration-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-wesa4OEvRQdxNzynk5ugU7ZRy0Ghkoaa6NmRGTqOASIng1hVaE3EKKO3rK11b4Y/pR3HUPIPKs1mRSnRCjHBfg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"prettier": "^2.8.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"custom-element-vs-code-integration": {
|
"custom-element-vs-code-integration": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
|
|||||||
25
package.json
25
package.json
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@shoelace-style/shoelace",
|
"name": "@shoelace-style/shoelace",
|
||||||
"description": "A forward-thinking library of web components.",
|
"description": "A forward-thinking library of web components.",
|
||||||
"version": "2.6.0",
|
"version": "2.8.0",
|
||||||
"homepage": "https://github.com/shoelace-style/shoelace",
|
"homepage": "https://github.com/shoelace-style/shoelace",
|
||||||
"author": "Cory LaViska",
|
"author": "Cory LaViska",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"customElements": "dist/custom-elements.json",
|
"customElements": "dist/custom-elements.json",
|
||||||
"web-types": "dist/web-types.json",
|
"web-types": "./web-types.json",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "dist/shoelace.d.ts",
|
"types": "dist/shoelace.d.ts",
|
||||||
"jsdelivr": "./cdn/shoelace-autoloader.js",
|
"jsdelivr": "./cdn/shoelace-autoloader.js",
|
||||||
@@ -25,15 +25,8 @@
|
|||||||
"./dist/react/*": "./dist/react/*",
|
"./dist/react/*": "./dist/react/*",
|
||||||
"./dist/translations/*": "./dist/translations/*"
|
"./dist/translations/*": "./dist/translations/*"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": ["dist", "cdn"],
|
||||||
"dist",
|
"keywords": ["web components", "custom elements", "components"],
|
||||||
"cdn"
|
|
||||||
],
|
|
||||||
"keywords": [
|
|
||||||
"web components",
|
|
||||||
"custom elements",
|
|
||||||
"components"
|
|
||||||
],
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/shoelace-style/shoelace.git"
|
"url": "git+https://github.com/shoelace-style/shoelace.git"
|
||||||
@@ -67,9 +60,9 @@
|
|||||||
"node": ">=14.17.0"
|
"node": ">=14.17.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ctrl/tinycolor": "^3.5.0",
|
"@ctrl/tinycolor": "^4.0.1",
|
||||||
"@floating-ui/dom": "^1.2.1",
|
"@floating-ui/dom": "^1.2.1",
|
||||||
"@lit-labs/react": "^1.1.1",
|
"@lit-labs/react": "^2.0.1",
|
||||||
"@shoelace-style/animations": "^1.1.0",
|
"@shoelace-style/animations": "^1.1.0",
|
||||||
"@shoelace-style/localize": "^3.1.1",
|
"@shoelace-style/localize": "^3.1.1",
|
||||||
"composed-offset-position": "^0.0.4",
|
"composed-offset-position": "^0.0.4",
|
||||||
@@ -95,6 +88,7 @@
|
|||||||
"command-line-args": "^5.2.1",
|
"command-line-args": "^5.2.1",
|
||||||
"comment-parser": "^1.3.1",
|
"comment-parser": "^1.3.1",
|
||||||
"cspell": "^6.18.1",
|
"cspell": "^6.18.1",
|
||||||
|
"custom-element-jet-brains-integration": "^1.1.0",
|
||||||
"custom-element-vs-code-integration": "^1.1.0",
|
"custom-element-vs-code-integration": "^1.1.0",
|
||||||
"del": "^7.0.0",
|
"del": "^7.0.0",
|
||||||
"download": "^8.0.0",
|
"download": "^8.0.0",
|
||||||
@@ -139,9 +133,6 @@
|
|||||||
"user-agent-data-types": "^0.3.0"
|
"user-agent-data-types": "^0.3.0"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{ts,js}": [
|
"*.{ts,js}": ["eslint --max-warnings 0 --cache --fix", "prettier --write"]
|
||||||
"eslint --max-warnings 0 --cache --fix",
|
|
||||||
"prettier --write"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ async function buildTheDocs(watch = false) {
|
|||||||
output.push(data.toString());
|
output.push(data.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
child.stderr.on('data', data => {
|
||||||
|
output.push(data.toString());
|
||||||
|
});
|
||||||
|
|
||||||
if (watch) {
|
if (watch) {
|
||||||
// The process doesn't terminate in watch mode so, before resolving, we listen for a known signal in stdout that
|
// The process doesn't terminate in watch mode so, before resolving, we listen for a known signal in stdout that
|
||||||
// tells us when the first build completes.
|
// tells us when the first build completes.
|
||||||
@@ -188,10 +192,6 @@ await nextTask('Wrapping components for React', () => {
|
|||||||
return execPromise(`node scripts/make-react.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
return execPromise(`node scripts/make-react.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||||
});
|
});
|
||||||
|
|
||||||
await nextTask('Generating Web Types', () => {
|
|
||||||
return execPromise(`node scripts/make-web-types.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
|
||||||
});
|
|
||||||
|
|
||||||
await nextTask('Generating themes', () => {
|
await nextTask('Generating themes', () => {
|
||||||
return execPromise(`node scripts/make-themes.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
return execPromise(`node scripts/make-themes.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||||
});
|
});
|
||||||
@@ -207,6 +207,7 @@ await nextTask('Running the TypeScript compiler', () => {
|
|||||||
// Copy the above steps to the CDN directory directly so we don't need to twice the work for nothing.
|
// Copy the above steps to the CDN directory directly so we don't need to twice the work for nothing.
|
||||||
await nextTask(`Copying Web Types, Themes, Icons, and TS Types to "${cdndir}"`, async () => {
|
await nextTask(`Copying Web Types, Themes, Icons, and TS Types to "${cdndir}"`, async () => {
|
||||||
await deleteAsync(cdndir);
|
await deleteAsync(cdndir);
|
||||||
|
await copy('./web-types.json', `${outdir}/web-types.json`);
|
||||||
await copy(outdir, cdndir);
|
await copy(outdir, cdndir);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -52,8 +52,10 @@ components.map(component => {
|
|||||||
${eventExports}
|
${eventExports}
|
||||||
|
|
||||||
const tagName = '${component.tagName}'
|
const tagName = '${component.tagName}'
|
||||||
|
Component.define('${component.tagName}')
|
||||||
|
|
||||||
const component = createComponent({
|
${jsDoc}
|
||||||
|
const reactWrapper = createComponent({
|
||||||
tagName,
|
tagName,
|
||||||
elementClass: Component,
|
elementClass: Component,
|
||||||
react: React,
|
react: React,
|
||||||
@@ -63,20 +65,7 @@ components.map(component => {
|
|||||||
displayName: "${component.name}"
|
displayName: "${component.name}"
|
||||||
})
|
})
|
||||||
|
|
||||||
${jsDoc}
|
export default reactWrapper
|
||||||
class SlComponent extends React.Component<Parameters<typeof component>[0]> {
|
|
||||||
constructor (...args: Parameters<typeof component>) {
|
|
||||||
super(...args)
|
|
||||||
Component.define(tagName)
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { children, ...props } = this.props
|
|
||||||
return React.createElement(component, props, children)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SlComponent;
|
|
||||||
`,
|
`,
|
||||||
Object.assign(prettierConfig, {
|
Object.assign(prettierConfig, {
|
||||||
parser: 'babel-ts'
|
parser: 'babel-ts'
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
//
|
|
||||||
// This script generates a web-types.json file from custom-elements.json for use with WebStorm/PHPStorm
|
|
||||||
//
|
|
||||||
// Docs: https://github.com/JetBrains/web-types
|
|
||||||
//
|
|
||||||
import commandLineArgs from 'command-line-args';
|
|
||||||
import jsonata from 'jsonata';
|
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
const { outdir } = commandLineArgs({ name: 'outdir', type: String });
|
|
||||||
const metadata = JSON.parse(fs.readFileSync(path.join(outdir, 'custom-elements.json'), 'utf8'));
|
|
||||||
|
|
||||||
const jsonataExprString = `{
|
|
||||||
"$schema": "http://json.schemastore.org/web-types",
|
|
||||||
"name": package.name,
|
|
||||||
"version": package.version,
|
|
||||||
"description-markup": "markdown",
|
|
||||||
"framework-config": {
|
|
||||||
"enable-when": {
|
|
||||||
"node-packages": [
|
|
||||||
package.name
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"contributions": {
|
|
||||||
"html": {
|
|
||||||
"elements": [
|
|
||||||
modules.declarations.{
|
|
||||||
"name": tagName,
|
|
||||||
"description": description,
|
|
||||||
"doc-url": $join(["https://shoelace.style/components/", $substringAfter(tagName, 'sl-')]),
|
|
||||||
"js": {
|
|
||||||
"properties": [
|
|
||||||
members.{
|
|
||||||
"name": name,
|
|
||||||
"description": description,
|
|
||||||
"value": {
|
|
||||||
"type": type.text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"events": [
|
|
||||||
events.{
|
|
||||||
"name": name,
|
|
||||||
"description": description
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"attributes": [
|
|
||||||
attributes.{
|
|
||||||
"name": name,
|
|
||||||
"description": description,
|
|
||||||
"value": {
|
|
||||||
"type": type.text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const expression = jsonata(jsonataExprString);
|
|
||||||
const result = await expression.evaluate(metadata);
|
|
||||||
|
|
||||||
fs.writeFileSync(path.join(outdir, 'web-types.json'), JSON.stringify(result, null, 2), 'utf8');
|
|
||||||
@@ -5,7 +5,7 @@ meta:
|
|||||||
layout: component
|
layout: component
|
||||||
---
|
---
|
||||||
|
|
||||||
```html preview
|
```html:preview
|
||||||
<{{ tag }}></{{ tag }}>
|
<{{ tag }}></{{ tag }}>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export default class SlButtonGroup extends ShoelaceElement {
|
|||||||
const index = slottedElements.indexOf(el);
|
const index = slottedElements.indexOf(el);
|
||||||
const button = findButton(el);
|
const button = findButton(el);
|
||||||
|
|
||||||
if (button !== null) {
|
if (button) {
|
||||||
button.classList.add('sl-button-group__button');
|
button.classList.add('sl-button-group__button');
|
||||||
button.classList.toggle('sl-button-group__button--first', index === 0);
|
button.classList.toggle('sl-button-group__button--first', index === 0);
|
||||||
button.classList.toggle('sl-button-group__button--inner', index > 0 && index < slottedElements.length - 1);
|
button.classList.toggle('sl-button-group__button--inner', index > 0 && index < slottedElements.length - 1);
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ import SlVisuallyHidden from '../visually-hidden/visually-hidden.component.js';
|
|||||||
import styles from './color-picker.styles.js';
|
import styles from './color-picker.styles.js';
|
||||||
import type { CSSResultGroup } from 'lit';
|
import type { CSSResultGroup } from 'lit';
|
||||||
import type { ShoelaceFormControl } from '../../internal/shoelace-element.js';
|
import type { ShoelaceFormControl } from '../../internal/shoelace-element.js';
|
||||||
import type SlChangeEvent from '../../events/sl-change.js';
|
import type { SlChangeEvent } from '../../events/sl-change.js';
|
||||||
import type SlInputEvent from '../../events/sl-input.js';
|
import type { SlInputEvent } from '../../events/sl-input.js';
|
||||||
|
|
||||||
const hasEyeDropper = 'EyeDropper' in window;
|
const hasEyeDropper = 'EyeDropper' in window;
|
||||||
|
|
||||||
@@ -882,7 +882,7 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
|||||||
style=${styleMap({
|
style=${styleMap({
|
||||||
backgroundImage: `linear-gradient(
|
backgroundImage: `linear-gradient(
|
||||||
to right,
|
to right,
|
||||||
${this.getHexString(this.hue, this.saturation, this.brightness, 0)} 0%
|
${this.getHexString(this.hue, this.saturation, this.brightness, 0)} 0%,
|
||||||
${this.getHexString(this.hue, this.saturation, this.brightness, 100)} 100%
|
${this.getHexString(this.hue, this.saturation, this.brightness, 100)} 100%
|
||||||
)`
|
)`
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -90,15 +90,15 @@ export default class SlDetails extends ShoelaceElement {
|
|||||||
this.detailsObserver.disconnect();
|
this.detailsObserver.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleSummaryClick(ev: MouseEvent) {
|
private handleSummaryClick(event: MouseEvent) {
|
||||||
ev.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
if (!this.disabled) {
|
if (!this.disabled) {
|
||||||
if (this.open) {
|
if (this.open) {
|
||||||
this.hide();
|
this.hide();
|
||||||
} else {
|
} else {
|
||||||
this.show();
|
this.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.header.focus();
|
this.header.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import '../../../dist/shoelace.js';
|
|||||||
// cspell:dictionaries lorem-ipsum
|
// cspell:dictionaries lorem-ipsum
|
||||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
import type { SlHideEvent } from '../../events/sl-hide';
|
||||||
|
import type { SlShowEvent } from '../../events/sl-show';
|
||||||
import type SlDetails from './details';
|
import type SlDetails from './details';
|
||||||
import type SlHideEvent from '../../events/sl-hide';
|
|
||||||
import type SlShowEvent from '../../events/sl-show';
|
|
||||||
|
|
||||||
describe('<sl-details>', () => {
|
describe('<sl-details>', () => {
|
||||||
describe('accessibility', () => {
|
describe('accessibility', () => {
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import ShoelaceElement from '../../internal/shoelace-element.js';
|
|||||||
import SlPopup from '../popup/popup.component.js';
|
import SlPopup from '../popup/popup.component.js';
|
||||||
import styles from './dropdown.styles.js';
|
import styles from './dropdown.styles.js';
|
||||||
import type { CSSResultGroup } from 'lit';
|
import type { CSSResultGroup } from 'lit';
|
||||||
|
import type { SlSelectEvent } from '../../events/sl-select.js';
|
||||||
import type SlButton from '../button/button.js';
|
import type SlButton from '../button/button.js';
|
||||||
import type SlIconButton from '../icon-button/icon-button.js';
|
import type SlIconButton from '../icon-button/icon-button.js';
|
||||||
import type SlMenu from '../menu/menu.js';
|
import type SlMenu from '../menu/menu.js';
|
||||||
import type SlSelectEvent from '../../events/sl-select.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Dropdowns expose additional content that "drops down" in a panel.
|
* @summary Dropdowns expose additional content that "drops down" in a panel.
|
||||||
|
|||||||
246
src/components/file-input/file-input.component.ts
Normal file
246
src/components/file-input/file-input.component.ts
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
import '../format-bytes/format-bytes.js';
|
||||||
|
import '../icon-button/icon-button.js';
|
||||||
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
|
import { defaultValue } from '../../internal/default-value.js';
|
||||||
|
import { FormControlController } from '../../internal/form.js';
|
||||||
|
import { HasSlotController } from '../../internal/slot.js';
|
||||||
|
import { html } from 'lit';
|
||||||
|
import { LocalizeController } from '../../utilities/localize.js';
|
||||||
|
import { property, query, state } from 'lit/decorators.js';
|
||||||
|
// import { watch } from '../../internal/watch';
|
||||||
|
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||||
|
import styles from './file-input.styles';
|
||||||
|
import type { CSSResultGroup } from 'lit';
|
||||||
|
import type { ShoelaceFormControl } from '../../internal/shoelace-element';
|
||||||
|
|
||||||
|
//
|
||||||
|
// TODO
|
||||||
|
//
|
||||||
|
// - button-only version
|
||||||
|
// - drag and drop support
|
||||||
|
// - localization
|
||||||
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Short summary of the component's intended use.
|
||||||
|
* @documentation https://shoelace.style/components/file-input
|
||||||
|
* @status experimental
|
||||||
|
* @since 2.0
|
||||||
|
*
|
||||||
|
* @dependency sl-format-bytes
|
||||||
|
* @dependency sl-icon-button
|
||||||
|
*
|
||||||
|
* @event sl-input - Emitted when the form control receives input.
|
||||||
|
*
|
||||||
|
* @slot label - The input's label. Alternatively, you can use the `label` attribute.
|
||||||
|
* @slot help-text - Text that describes how to use the input. Alternatively, you can use the `help-text` attribute.
|
||||||
|
|
||||||
|
* @csspart base - The component's base wrapper.
|
||||||
|
*/
|
||||||
|
export default class SlFileInput extends ShoelaceElement implements ShoelaceFormControl {
|
||||||
|
static styles: CSSResultGroup = styles;
|
||||||
|
|
||||||
|
private readonly formControlController = new FormControlController(this, {
|
||||||
|
value: (control: SlFileInput) => control.files,
|
||||||
|
assumeInteractionOn: ['sl-input']
|
||||||
|
});
|
||||||
|
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
||||||
|
private readonly localize = new LocalizeController(this);
|
||||||
|
|
||||||
|
@query('input[type="file"]') input: HTMLInputElement;
|
||||||
|
|
||||||
|
@state() private files: File[] = [];
|
||||||
|
@state() private hasFocus = false;
|
||||||
|
|
||||||
|
/** The name of the input, submitted as a name/value pair with form data. */
|
||||||
|
@property() name = '';
|
||||||
|
|
||||||
|
/** The current value of the input, submitted as a name/value pair with form data. */
|
||||||
|
@property() value = '';
|
||||||
|
|
||||||
|
/** The default value of the form control. Primarily used for resetting the form control. */
|
||||||
|
@defaultValue() defaultValue = '';
|
||||||
|
|
||||||
|
/** The input's size. */
|
||||||
|
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
|
||||||
|
|
||||||
|
/** Draws a filled input. */
|
||||||
|
@property({ type: Boolean, reflect: true }) filled = false;
|
||||||
|
|
||||||
|
/** Draws a pill-style input with rounded edges. */
|
||||||
|
@property({ type: Boolean, reflect: true }) pill = false;
|
||||||
|
|
||||||
|
/** The input's label. If you need to display HTML, use the `label` slot instead. */
|
||||||
|
@property() label = '';
|
||||||
|
|
||||||
|
/** The input's help text. If you need to display HTML, use the `help-text` slot instead. */
|
||||||
|
@property({ attribute: 'help-text' }) helpText = '';
|
||||||
|
|
||||||
|
/** Disables the input. */
|
||||||
|
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||||
|
|
||||||
|
/** A list of acceptable file types. Must be a comma-separated list of [unique file type specifiers](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#unique_file_type_specifiers). */
|
||||||
|
@property() accept = false;
|
||||||
|
|
||||||
|
/** Allows more than one file to be selected. */
|
||||||
|
@property({ type: Boolean }) multiple = false;
|
||||||
|
|
||||||
|
/** Gets the validity state object */
|
||||||
|
get validity() {
|
||||||
|
return this.input.validity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets the validation message */
|
||||||
|
get validationMessage() {
|
||||||
|
return this.input.validationMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleInput() {
|
||||||
|
// Append selected files
|
||||||
|
if (this.input.files) {
|
||||||
|
this.files = this.files.concat([...this.input.files]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the input
|
||||||
|
this.input.value = '';
|
||||||
|
|
||||||
|
this.emit('sl-input');
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleRemoveClick(_: MouseEvent, indexToRemove: number) {
|
||||||
|
this.files = this.files.filter((__, index) => index !== indexToRemove);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isImage(file: File) {
|
||||||
|
return ['image/gif', 'image/jpeg', 'image/png', 'image/svg+xml', 'image/webp'].includes(file.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Checks for validity but does not show a validation message. Returns `true` when valid and `false` when invalid. */
|
||||||
|
checkValidity() {
|
||||||
|
return this.input.checkValidity();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gets the associated form, if one exists. */
|
||||||
|
getForm(): HTMLFormElement | null {
|
||||||
|
return this.formControlController.getForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||||
|
reportValidity() {
|
||||||
|
return this.input.reportValidity();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets a custom validation message. Pass an empty string to restore validity. */
|
||||||
|
setCustomValidity(message: string) {
|
||||||
|
this.input.setCustomValidity(message);
|
||||||
|
this.formControlController.updateValidity();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const hasLabelSlot = this.hasSlotController.test('label');
|
||||||
|
const hasHelpTextSlot = this.hasSlotController.test('help-text');
|
||||||
|
const hasLabel = this.label ? true : !!hasLabelSlot;
|
||||||
|
const hasHelpText = this.helpText ? true : !!hasHelpTextSlot;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
part="form-control"
|
||||||
|
class=${classMap({
|
||||||
|
'form-control': true,
|
||||||
|
'form-control--small': this.size === 'small',
|
||||||
|
'form-control--medium': this.size === 'medium',
|
||||||
|
'form-control--large': this.size === 'large',
|
||||||
|
'form-control--has-label': hasLabel,
|
||||||
|
'form-control--has-help-text': hasHelpText
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
part="form-control-label"
|
||||||
|
class="form-control__label"
|
||||||
|
for="input"
|
||||||
|
aria-hidden=${hasLabel ? 'false' : 'true'}
|
||||||
|
>
|
||||||
|
<slot name="label">${this.label}</slot>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div part="form-control-input" class="form-control-input">
|
||||||
|
<div
|
||||||
|
part="base"
|
||||||
|
class=${classMap({
|
||||||
|
input: true,
|
||||||
|
|
||||||
|
// Sizes
|
||||||
|
'input--small': this.size === 'small',
|
||||||
|
'input--medium': this.size === 'medium',
|
||||||
|
'input--large': this.size === 'large',
|
||||||
|
|
||||||
|
// States
|
||||||
|
'input--pill': this.pill,
|
||||||
|
'input--standard': !this.filled,
|
||||||
|
'input--filled': this.filled,
|
||||||
|
'input--disabled': this.disabled,
|
||||||
|
'input--focused': this.hasFocus,
|
||||||
|
'input--empty': !this.value
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="input"
|
||||||
|
class="input__control"
|
||||||
|
name=${this.name}
|
||||||
|
type="file"
|
||||||
|
aria-describedby="help-text"
|
||||||
|
?multiple=${this.multiple}
|
||||||
|
@input=${this.handleInput}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<sl-button @click=${() => this.input.click()}>Choose Files</sl-button>
|
||||||
|
|
||||||
|
<div class="input__files">
|
||||||
|
${this.files.map((file, index) => {
|
||||||
|
const isImage = this.isImage(file);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="input__file">
|
||||||
|
<span class="input__file-preview">
|
||||||
|
${isImage
|
||||||
|
? html`<img
|
||||||
|
class="input__file-preview-image input__file-preview--image"
|
||||||
|
src=${URL.createObjectURL(file)}
|
||||||
|
alt="${file.name}"
|
||||||
|
/>`
|
||||||
|
: html``}
|
||||||
|
</span>
|
||||||
|
<span class="input__file-name">${file.name}</span>
|
||||||
|
<span class="input__file-size">
|
||||||
|
<sl-format-bytes
|
||||||
|
value=${file.size}
|
||||||
|
display="short"
|
||||||
|
lang=${this.localize.lang()}
|
||||||
|
></sl-format-bytes>
|
||||||
|
</span>
|
||||||
|
<sl-icon-button
|
||||||
|
class="input__file-remove"
|
||||||
|
name="x-lg"
|
||||||
|
library="system"
|
||||||
|
label=${this.localize.term('remove')}
|
||||||
|
@click=${(event: MouseEvent) => this.handleRemoveClick(event, index)}
|
||||||
|
></sl-icon-button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
part="form-control-help-text"
|
||||||
|
id="help-text"
|
||||||
|
class="form-control__help-text"
|
||||||
|
aria-hidden=${hasHelpText ? 'false' : 'true'}
|
||||||
|
>
|
||||||
|
<slot name="help-text">${this.helpText}</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/components/file-input/file-input.styles.ts
Normal file
62
src/components/file-input/file-input.styles.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { css } from 'lit';
|
||||||
|
import componentStyles from '../../styles/component.styles';
|
||||||
|
|
||||||
|
export default css`
|
||||||
|
${componentStyles}
|
||||||
|
|
||||||
|
:host {
|
||||||
|
--preview-size: 4rem;
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input__control {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
clip-path: inset(50%);
|
||||||
|
border: none;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input__files:not(:empty) {
|
||||||
|
margin-block-start: var(--sl-spacing-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input__file {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input__file-preview {
|
||||||
|
position: relative;
|
||||||
|
width: var(--preview-size);
|
||||||
|
height: var(--preview-size);
|
||||||
|
margin-inline-end: var(--sl-spacing-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input__file-preview-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input__file-name {
|
||||||
|
}
|
||||||
|
|
||||||
|
.input__file-size {
|
||||||
|
font-size: var(--sl-font-size-small);
|
||||||
|
color: var(--sl-color-neutral-800);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input__file-size::before {
|
||||||
|
content: '(';
|
||||||
|
}
|
||||||
|
|
||||||
|
.input__file-size::after {
|
||||||
|
content: ')';
|
||||||
|
}
|
||||||
|
`;
|
||||||
9
src/components/file-input/file-input.test.ts
Normal file
9
src/components/file-input/file-input.test.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { expect, fixture, html } from '@open-wc/testing';
|
||||||
|
|
||||||
|
describe('<sl-file-input>', () => {
|
||||||
|
it('should render a component', async () => {
|
||||||
|
const el = await fixture(html` <sl-file-input></sl-file-input> `);
|
||||||
|
|
||||||
|
expect(el).to.exist;
|
||||||
|
});
|
||||||
|
});
|
||||||
12
src/components/file-input/file-input.ts
Normal file
12
src/components/file-input/file-input.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import SlFileInput from './file-input.component.js';
|
||||||
|
|
||||||
|
export * from './file-input.component.js';
|
||||||
|
export default SlFileInput;
|
||||||
|
|
||||||
|
SlFileInput.define('sl-file-input');
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'sl-file-input': SlFileInput;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { aTimeout, elementUpdated, expect, fixture, html, oneEvent } from '@open-wc/testing';
|
import { aTimeout, elementUpdated, expect, fixture, html, oneEvent } from '@open-wc/testing';
|
||||||
import { registerIconLibrary } from '../../../dist/shoelace.js';
|
import { registerIconLibrary } from '../../../dist/shoelace.js';
|
||||||
import type SlErrorEvent from '../../events/sl-error';
|
import type { SlErrorEvent } from '../../events/sl-error';
|
||||||
|
import type { SlLoadEvent } from '../../events/sl-load';
|
||||||
import type SlIcon from './icon';
|
import type SlIcon from './icon';
|
||||||
import type SlLoadEvent from '../../events/sl-load';
|
|
||||||
|
|
||||||
const testLibraryIcons = {
|
const testLibraryIcons = {
|
||||||
'test-icon1': `
|
'test-icon1': `
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { getTextContent } from '../../internal/slot.js';
|
import { getTextContent, HasSlotController } from '../../internal/slot.js';
|
||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
|
import { LocalizeController } from '../../utilities/localize.js';
|
||||||
import { property, query } from 'lit/decorators.js';
|
import { property, query } from 'lit/decorators.js';
|
||||||
|
import { SubmenuController } from './submenu-controller.js';
|
||||||
import { watch } from '../../internal/watch.js';
|
import { watch } from '../../internal/watch.js';
|
||||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||||
import SlIcon from '../icon/icon.component.js';
|
import SlIcon from '../icon/icon.component.js';
|
||||||
|
import SlPopup from '../popup/popup.component.js';
|
||||||
import styles from './menu-item.styles.js';
|
import styles from './menu-item.styles.js';
|
||||||
import type { CSSResultGroup } from 'lit';
|
import type { CSSResultGroup } from 'lit';
|
||||||
|
|
||||||
@@ -15,10 +18,12 @@ import type { CSSResultGroup } from 'lit';
|
|||||||
* @since 2.0
|
* @since 2.0
|
||||||
*
|
*
|
||||||
* @dependency sl-icon
|
* @dependency sl-icon
|
||||||
|
* @dependency sl-popup
|
||||||
*
|
*
|
||||||
* @slot - The menu item's label.
|
* @slot - The menu item's label.
|
||||||
* @slot prefix - Used to prepend an icon or similar element to the menu item.
|
* @slot prefix - Used to prepend an icon or similar element to the menu item.
|
||||||
* @slot suffix - Used to append an icon or similar element to the menu item.
|
* @slot suffix - Used to append an icon or similar element to the menu item.
|
||||||
|
* @slot submenu - Used to denote a nested menu.
|
||||||
*
|
*
|
||||||
* @csspart base - The component's base wrapper.
|
* @csspart base - The component's base wrapper.
|
||||||
* @csspart checked-icon - The checked icon, which is only visible when the menu item is checked.
|
* @csspart checked-icon - The checked icon, which is only visible when the menu item is checked.
|
||||||
@@ -26,10 +31,15 @@ import type { CSSResultGroup } from 'lit';
|
|||||||
* @csspart label - The menu item label.
|
* @csspart label - The menu item label.
|
||||||
* @csspart suffix - The suffix container.
|
* @csspart suffix - The suffix container.
|
||||||
* @csspart submenu-icon - The submenu icon, visible only when the menu item has a submenu (not yet implemented).
|
* @csspart submenu-icon - The submenu icon, visible only when the menu item has a submenu (not yet implemented).
|
||||||
|
*
|
||||||
|
* @cssproperty [--submenu-offset=-2px] - The distance submenus shift to overlap the parent menu.
|
||||||
*/
|
*/
|
||||||
export default class SlMenuItem extends ShoelaceElement {
|
export default class SlMenuItem extends ShoelaceElement {
|
||||||
static styles: CSSResultGroup = styles;
|
static styles: CSSResultGroup = styles;
|
||||||
static dependencies = { 'sl-icon': SlIcon };
|
static dependencies = {
|
||||||
|
'sl-icon': SlIcon,
|
||||||
|
'sl-popup': SlPopup
|
||||||
|
};
|
||||||
|
|
||||||
private cachedTextLabel: string;
|
private cachedTextLabel: string;
|
||||||
|
|
||||||
@@ -48,6 +58,22 @@ export default class SlMenuItem extends ShoelaceElement {
|
|||||||
/** Draws the menu item in a disabled state, preventing selection. */
|
/** Draws the menu item in a disabled state, preventing selection. */
|
||||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||||
|
|
||||||
|
private readonly localize = new LocalizeController(this);
|
||||||
|
private readonly hasSlotController = new HasSlotController(this, 'submenu');
|
||||||
|
private submenuController: SubmenuController = new SubmenuController(this, this.hasSlotController, this.localize);
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
super.connectedCallback();
|
||||||
|
this.addEventListener('click', this.handleHostClick);
|
||||||
|
this.addEventListener('mouseover', this.handleMouseOver);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this.removeEventListener('click', this.handleHostClick);
|
||||||
|
this.removeEventListener('mouseover', this.handleMouseOver);
|
||||||
|
}
|
||||||
|
|
||||||
private handleDefaultSlotChange() {
|
private handleDefaultSlotChange() {
|
||||||
const textLabel = this.getTextLabel();
|
const textLabel = this.getTextLabel();
|
||||||
|
|
||||||
@@ -64,6 +90,19 @@ export default class SlMenuItem extends ShoelaceElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleHostClick = (event: MouseEvent) => {
|
||||||
|
// Prevent the click event from being emitted when the button is disabled or loading
|
||||||
|
if (this.disabled) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleMouseOver = (event: MouseEvent) => {
|
||||||
|
this.focus();
|
||||||
|
event.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
@watch('checked')
|
@watch('checked')
|
||||||
handleCheckedChange() {
|
handleCheckedChange() {
|
||||||
// For proper accessibility, users have to use type="checkbox" to use the checked attribute
|
// For proper accessibility, users have to use type="checkbox" to use the checked attribute
|
||||||
@@ -102,16 +141,28 @@ export default class SlMenuItem extends ShoelaceElement {
|
|||||||
return getTextContent(this.defaultSlot);
|
return getTextContent(this.defaultSlot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isSubmenu() {
|
||||||
|
return this.hasSlotController.test('submenu');
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const isRtl = this.localize.dir() === 'rtl';
|
||||||
|
const isSubmenuExpanded = this.submenuController.isExpanded();
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
|
id="anchor"
|
||||||
part="base"
|
part="base"
|
||||||
class=${classMap({
|
class=${classMap({
|
||||||
'menu-item': true,
|
'menu-item': true,
|
||||||
|
'menu-item--rtl': isRtl,
|
||||||
'menu-item--checked': this.checked,
|
'menu-item--checked': this.checked,
|
||||||
'menu-item--disabled': this.disabled,
|
'menu-item--disabled': this.disabled,
|
||||||
'menu-item--has-submenu': false // reserved for future use
|
'menu-item--has-submenu': this.isSubmenu(),
|
||||||
|
'menu-item--submenu-expanded': isSubmenuExpanded
|
||||||
})}
|
})}
|
||||||
|
?aria-haspopup="${this.isSubmenu()}"
|
||||||
|
?aria-expanded="${isSubmenuExpanded ? true : false}"
|
||||||
>
|
>
|
||||||
<span part="checked-icon" class="menu-item__check">
|
<span part="checked-icon" class="menu-item__check">
|
||||||
<sl-icon name="check" library="system" aria-hidden="true"></sl-icon>
|
<sl-icon name="check" library="system" aria-hidden="true"></sl-icon>
|
||||||
@@ -124,8 +175,10 @@ export default class SlMenuItem extends ShoelaceElement {
|
|||||||
<slot name="suffix" part="suffix" class="menu-item__suffix"></slot>
|
<slot name="suffix" part="suffix" class="menu-item__suffix"></slot>
|
||||||
|
|
||||||
<span part="submenu-icon" class="menu-item__chevron">
|
<span part="submenu-icon" class="menu-item__chevron">
|
||||||
<sl-icon name="chevron-right" library="system" aria-hidden="true"></sl-icon>
|
<sl-icon name=${isRtl ? 'chevron-left' : 'chevron-right'} library="system" aria-hidden="true"></sl-icon>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
${this.submenuController.renderSubmenu()}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ export default css`
|
|||||||
${componentStyles}
|
${componentStyles}
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
|
--submenu-offset: -2px;
|
||||||
|
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +40,8 @@ export default css`
|
|||||||
.menu-item .menu-item__label {
|
.menu-item .menu-item__label {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-item .menu-item__prefix {
|
.menu-item .menu-item__prefix {
|
||||||
@@ -64,7 +68,8 @@ export default css`
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
:host(:hover:not([aria-disabled='true'], :focus-visible)) .menu-item {
|
:host(:hover:not([aria-disabled='true'], :focus-visible)) .menu-item,
|
||||||
|
.menu-item--submenu-expanded {
|
||||||
background-color: var(--sl-color-neutral-100);
|
background-color: var(--sl-color-neutral-100);
|
||||||
color: var(--sl-color-neutral-1000);
|
color: var(--sl-color-neutral-1000);
|
||||||
}
|
}
|
||||||
@@ -91,6 +96,17 @@ export default css`
|
|||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Add elevation and z-index to submenus */
|
||||||
|
sl-popup::part(popup) {
|
||||||
|
box-shadow: var(--sl-shadow-large);
|
||||||
|
z-index: var(--sl-z-index-dropdown);
|
||||||
|
margin-left: var(--submenu-offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item--rtl sl-popup::part(popup) {
|
||||||
|
margin-left: calc(-1 * var(--submenu-offset));
|
||||||
|
}
|
||||||
|
|
||||||
@media (forced-colors: active) {
|
@media (forced-colors: active) {
|
||||||
:host(:hover:not([aria-disabled='true'])) .menu-item,
|
:host(:hover:not([aria-disabled='true'])) .menu-item,
|
||||||
:host(:focus-visible) .menu-item {
|
:host(:focus-visible) .menu-item {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import '../../../dist/shoelace.js';
|
import '../../../dist/shoelace.js';
|
||||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||||
|
import { sendKeys } from '@web/test-runner-commands';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
import type { SlSelectEvent } from '../../events/sl-select';
|
||||||
import type SlMenuItem from './menu-item';
|
import type SlMenuItem from './menu-item';
|
||||||
|
|
||||||
describe('<sl-menu-item>', () => {
|
describe('<sl-menu-item>', () => {
|
||||||
@@ -18,6 +20,21 @@ describe('<sl-menu-item>', () => {
|
|||||||
await expect(el).to.be.accessible();
|
await expect(el).to.be.accessible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests when using a submenu', async () => {
|
||||||
|
const el = await fixture<SlMenuItem>(html`
|
||||||
|
<sl-menu>
|
||||||
|
<sl-menu-item>
|
||||||
|
Submenu
|
||||||
|
<sl-menu slot="submenu">
|
||||||
|
<sl-menu-item>Submenu Item 1</sl-menu-item>
|
||||||
|
<sl-menu-item>Submenu Item 2</sl-menu-item>
|
||||||
|
</sl-menu>
|
||||||
|
</sl-menu-item>
|
||||||
|
</sl-menu>
|
||||||
|
`);
|
||||||
|
await expect(el).to.be.accessible();
|
||||||
|
});
|
||||||
|
|
||||||
it('should have the correct default properties', async () => {
|
it('should have the correct default properties', async () => {
|
||||||
const el = await fixture<SlMenuItem>(html` <sl-menu-item>Test</sl-menu-item> `);
|
const el = await fixture<SlMenuItem>(html` <sl-menu-item>Test</sl-menu-item> `);
|
||||||
|
|
||||||
@@ -59,4 +76,98 @@ describe('<sl-menu-item>', () => {
|
|||||||
|
|
||||||
expect(getComputedStyle(item1).display).to.equal('none');
|
expect(getComputedStyle(item1).display).to.equal('none');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not render a sl-popup if the slot="submenu" attribute is missing, but the slot should exist in the component and be hidden.', async () => {
|
||||||
|
const menu = await fixture<SlMenuItem>(html`
|
||||||
|
<sl-menu>
|
||||||
|
<sl-menu-item>
|
||||||
|
Item 1
|
||||||
|
<sl-menu>
|
||||||
|
<sl-menu-item> Nested Item 1 </sl-menu-item>
|
||||||
|
</sl-menu>
|
||||||
|
</sl-menu-item>
|
||||||
|
</sl-menu>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const menuItem: HTMLElement = menu.querySelector('sl-menu-item')!;
|
||||||
|
expect(menuItem.shadowRoot!.querySelector('sl-popup')).to.be.null;
|
||||||
|
const submenuSlot: HTMLElement = menuItem.shadowRoot!.querySelector('slot[name="submenu"]')!;
|
||||||
|
expect(submenuSlot.hidden).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render an sl-popup if the slot="submenu" attribute is present', async () => {
|
||||||
|
const menu = await fixture<SlMenuItem>(html`
|
||||||
|
<sl-menu>
|
||||||
|
<sl-menu-item id="test">
|
||||||
|
Item 1
|
||||||
|
<sl-menu slot="submenu">
|
||||||
|
<sl-menu-item> Nested Item 1 </sl-menu-item>
|
||||||
|
</sl-menu>
|
||||||
|
</sl-menu-item>
|
||||||
|
</sl-menu>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const menuItem = menu.querySelector('sl-menu-item')!;
|
||||||
|
expect(menuItem.shadowRoot!.querySelector('sl-popup')).to.be.not.null;
|
||||||
|
const submenuSlot: HTMLElement = menuItem.shadowRoot!.querySelector('slot[name="submenu"]')!;
|
||||||
|
expect(submenuSlot.hidden).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should focus on first menuitem of submenu if ArrowRight is pressed on parent menuitem', async () => {
|
||||||
|
const menu = await fixture<SlMenuItem>(html`
|
||||||
|
<sl-menu>
|
||||||
|
<sl-menu-item id="item-1">
|
||||||
|
Submenu
|
||||||
|
<sl-menu slot="submenu">
|
||||||
|
<sl-menu-item value="submenu-item-1"> Nested Item 1 </sl-menu-item>
|
||||||
|
</sl-menu>
|
||||||
|
</sl-menu-item>
|
||||||
|
</sl-menu>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const selectHandler = sinon.spy((event: SlSelectEvent) => {
|
||||||
|
const item = event.detail.item;
|
||||||
|
expect(item.value).to.equal('submenu-item-1');
|
||||||
|
});
|
||||||
|
menu.addEventListener('sl-select', selectHandler);
|
||||||
|
|
||||||
|
const submenu = menu.querySelector('sl-menu-item');
|
||||||
|
submenu!.focus();
|
||||||
|
await menu.updateComplete;
|
||||||
|
await sendKeys({ press: 'ArrowRight' });
|
||||||
|
await menu.updateComplete;
|
||||||
|
await sendKeys({ press: 'Enter' });
|
||||||
|
await menu.updateComplete;
|
||||||
|
// Once for each menu element.
|
||||||
|
expect(selectHandler).to.have.been.calledTwice;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should focus on outer menu if ArrowRight is pressed on nested menuitem', async () => {
|
||||||
|
const menu = await fixture<SlMenuItem>(html`
|
||||||
|
<sl-menu>
|
||||||
|
<sl-menu-item value="outer-item-1">
|
||||||
|
Submenu
|
||||||
|
<sl-menu slot="submenu">
|
||||||
|
<sl-menu-item value="inner-item-1"> Nested Item 1 </sl-menu-item>
|
||||||
|
</sl-menu>
|
||||||
|
</sl-menu-item>
|
||||||
|
</sl-menu>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const focusHandler = sinon.spy((event: FocusEvent) => {
|
||||||
|
expect(event.target.value).to.equal('outer-item-1');
|
||||||
|
expect(event.relatedTarget.value).to.equal('inner-item-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
const outerItem = menu.querySelector('sl-menu-item');
|
||||||
|
outerItem!.focus();
|
||||||
|
await menu.updateComplete;
|
||||||
|
await sendKeys({ press: 'ArrowRight' });
|
||||||
|
|
||||||
|
outerItem.addEventListener('focus', focusHandler);
|
||||||
|
await menu.updateComplete;
|
||||||
|
await sendKeys({ press: 'ArrowLeft' });
|
||||||
|
await menu.updateComplete;
|
||||||
|
expect(focusHandler).to.have.been.calledOnce;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
262
src/components/menu-item/submenu-controller.ts
Normal file
262
src/components/menu-item/submenu-controller.ts
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
import { createRef, ref, type Ref } from 'lit/directives/ref.js';
|
||||||
|
import { type HasSlotController } from '../../internal/slot.js';
|
||||||
|
import { html } from 'lit';
|
||||||
|
import { type LocalizeController } from '../../utilities/localize.js';
|
||||||
|
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
||||||
|
import type SlMenuItem from './menu-item.js';
|
||||||
|
import type SlPopup from '../popup/popup.js';
|
||||||
|
|
||||||
|
/** A reactive controller to manage the registration of event listeners for submenus. */
|
||||||
|
export class SubmenuController implements ReactiveController {
|
||||||
|
private host: ReactiveControllerHost & SlMenuItem;
|
||||||
|
private popupRef: Ref<SlPopup> = createRef();
|
||||||
|
private enableSubmenuTimer = -1;
|
||||||
|
private isConnected = false;
|
||||||
|
private isPopupConnected = false;
|
||||||
|
private skidding = 0;
|
||||||
|
private readonly hasSlotController: HasSlotController;
|
||||||
|
private readonly localize: LocalizeController;
|
||||||
|
private readonly submenuOpenDelay = 100;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
host: ReactiveControllerHost & SlMenuItem,
|
||||||
|
hasSlotController: HasSlotController,
|
||||||
|
localize: LocalizeController
|
||||||
|
) {
|
||||||
|
(this.host = host).addController(this);
|
||||||
|
this.hasSlotController = hasSlotController;
|
||||||
|
this.localize = localize;
|
||||||
|
}
|
||||||
|
|
||||||
|
hostConnected() {
|
||||||
|
if (this.hasSlotController.test('submenu') && !this.host.disabled) {
|
||||||
|
this.addListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hostDisconnected() {
|
||||||
|
this.removeListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
hostUpdated() {
|
||||||
|
if (this.hasSlotController.test('submenu') && !this.host.disabled) {
|
||||||
|
this.addListeners();
|
||||||
|
this.updateSkidding();
|
||||||
|
} else {
|
||||||
|
this.removeListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private addListeners() {
|
||||||
|
if (!this.isConnected) {
|
||||||
|
this.host.addEventListener('mouseover', this.handleMouseOver);
|
||||||
|
this.host.addEventListener('keydown', this.handleKeyDown);
|
||||||
|
this.host.addEventListener('click', this.handleClick);
|
||||||
|
this.host.addEventListener('focusout', this.handleFocusOut);
|
||||||
|
this.isConnected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The popup does not seem to get wired when the host is
|
||||||
|
// connected, so manage its listeners separately.
|
||||||
|
if (!this.isPopupConnected) {
|
||||||
|
if (this.popupRef.value) {
|
||||||
|
this.popupRef.value.addEventListener('mouseover', this.handlePopupMouseover);
|
||||||
|
this.isPopupConnected = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeListeners() {
|
||||||
|
if (this.isConnected) {
|
||||||
|
this.host.removeEventListener('mouseover', this.handleMouseOver);
|
||||||
|
this.host.removeEventListener('keydown', this.handleKeyDown);
|
||||||
|
this.host.removeEventListener('click', this.handleClick);
|
||||||
|
this.host.removeEventListener('focusout', this.handleFocusOut);
|
||||||
|
this.isConnected = false;
|
||||||
|
}
|
||||||
|
if (this.isPopupConnected) {
|
||||||
|
if (this.popupRef.value) {
|
||||||
|
this.popupRef.value.removeEventListener('mouseover', this.handlePopupMouseover);
|
||||||
|
this.isPopupConnected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMouseOver = () => {
|
||||||
|
if (this.hasSlotController.test('submenu')) {
|
||||||
|
this.enableSubmenu();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleSubmenuEntry(event: KeyboardEvent) {
|
||||||
|
// Pass focus to the first menu-item in the submenu.
|
||||||
|
const submenuSlot: HTMLSlotElement | null = this.host.renderRoot.querySelector("slot[name='submenu']");
|
||||||
|
|
||||||
|
// Missing slot
|
||||||
|
if (!submenuSlot) {
|
||||||
|
console.error('Cannot activate a submenu if no corresponding menuitem can be found.', this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menus
|
||||||
|
let menuItems: NodeListOf<Element> | null = null;
|
||||||
|
for (const elt of submenuSlot.assignedElements()) {
|
||||||
|
menuItems = elt.querySelectorAll("sl-menu-item, [role^='menuitem']");
|
||||||
|
if (menuItems.length !== 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!menuItems || menuItems.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
menuItems[0].setAttribute('tabindex', '0');
|
||||||
|
for (let i = 1; i !== menuItems.length; ++i) {
|
||||||
|
menuItems[i].setAttribute('tabindex', '-1');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the submenu (if not open), and set focus to first menuitem.
|
||||||
|
if (this.popupRef.value) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
if (this.popupRef.value.active) {
|
||||||
|
if (menuItems[0] instanceof HTMLElement) {
|
||||||
|
menuItems[0].focus();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.enableSubmenu(false);
|
||||||
|
this.host.updateComplete.then(() => {
|
||||||
|
if (menuItems![0] instanceof HTMLElement) {
|
||||||
|
menuItems![0].focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.host.requestUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus on the first menu-item of a submenu.
|
||||||
|
private handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
switch (event.key) {
|
||||||
|
case 'Escape':
|
||||||
|
case 'Tab':
|
||||||
|
this.disableSubmenu();
|
||||||
|
break;
|
||||||
|
case 'ArrowLeft':
|
||||||
|
// Either focus is currently on the host element or a child
|
||||||
|
if (event.target !== this.host) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
this.host.focus();
|
||||||
|
this.disableSubmenu();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ArrowRight':
|
||||||
|
case 'Enter':
|
||||||
|
case ' ':
|
||||||
|
this.handleSubmenuEntry(event);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleClick = (event: MouseEvent) => {
|
||||||
|
// Clicking on the item which heads the menu does nothing, otherwise hide submenu and propagate
|
||||||
|
if (event.target === this.host) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
} else if (
|
||||||
|
event.target instanceof Element &&
|
||||||
|
(event.target.tagName === 'sl-menu-item' || event.target.role?.startsWith('menuitem'))
|
||||||
|
) {
|
||||||
|
this.disableSubmenu();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Close this submenu on focus outside of the parent or any descendants.
|
||||||
|
private handleFocusOut = (event: FocusEvent) => {
|
||||||
|
if (event.relatedTarget && event.relatedTarget instanceof Element && this.host.contains(event.relatedTarget)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.disableSubmenu();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prevent the parent menu-item from getting focus on mouse movement on the submenu
|
||||||
|
private handlePopupMouseover = (event: MouseEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
private setSubmenuState(state: boolean) {
|
||||||
|
if (this.popupRef.value) {
|
||||||
|
if (this.popupRef.value.active !== state) {
|
||||||
|
this.popupRef.value.active = state;
|
||||||
|
this.host.requestUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shows the submenu. Supports disabling the opening delay, e.g. for keyboard events that want to set the focus to the
|
||||||
|
// newly opened menu.
|
||||||
|
private enableSubmenu(delay = true) {
|
||||||
|
if (delay) {
|
||||||
|
this.enableSubmenuTimer = window.setTimeout(() => {
|
||||||
|
this.setSubmenuState(true);
|
||||||
|
}, this.submenuOpenDelay);
|
||||||
|
} else {
|
||||||
|
this.setSubmenuState(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private disableSubmenu() {
|
||||||
|
clearTimeout(this.enableSubmenuTimer);
|
||||||
|
this.setSubmenuState(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the space the top of a menu takes-up, for aligning the popup menu-item with the activating element.
|
||||||
|
private updateSkidding(): void {
|
||||||
|
// .computedStyleMap() not always available.
|
||||||
|
if (!this.host.parentElement?.computedStyleMap) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const styleMap: StylePropertyMapReadOnly = this.host.parentElement.computedStyleMap();
|
||||||
|
const attrs: string[] = ['padding-top', 'border-top-width', 'margin-top'];
|
||||||
|
|
||||||
|
const skidding = attrs.reduce((accumulator, attr) => {
|
||||||
|
const styleValue: CSSStyleValue = styleMap.get(attr) ?? new CSSUnitValue(0, 'px');
|
||||||
|
const unitValue = styleValue instanceof CSSUnitValue ? styleValue : new CSSUnitValue(0, 'px');
|
||||||
|
const pxValue = unitValue.to('px');
|
||||||
|
return accumulator - pxValue.value;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
this.skidding = skidding;
|
||||||
|
}
|
||||||
|
|
||||||
|
isExpanded(): boolean {
|
||||||
|
return this.popupRef.value ? this.popupRef.value.active : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSubmenu() {
|
||||||
|
const isLtr = this.localize.dir() === 'ltr';
|
||||||
|
|
||||||
|
// Always render the slot, but conditionally render the outer <sl-popup>
|
||||||
|
if (!this.isConnected) {
|
||||||
|
return html` <slot name="submenu" hidden></slot> `;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<sl-popup
|
||||||
|
${ref(this.popupRef)}
|
||||||
|
placement=${isLtr ? 'right-start' : 'left-start'}
|
||||||
|
anchor="anchor"
|
||||||
|
flip
|
||||||
|
flip-fallback-strategy="best-fit"
|
||||||
|
skidding="${this.skidding}"
|
||||||
|
strategy="fixed"
|
||||||
|
>
|
||||||
|
<slot name="submenu"></slot>
|
||||||
|
</sl-popup>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
import { query } from 'lit/decorators.js';
|
import { query } from 'lit/decorators.js';
|
||||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||||
|
import SlMenuItem from '../menu-item/menu-item.component.js';
|
||||||
import styles from './menu.styles.js';
|
import styles from './menu.styles.js';
|
||||||
import type { CSSResultGroup } from 'lit';
|
import type { CSSResultGroup } from 'lit';
|
||||||
import type SlMenuItem from '../menu-item/menu-item.js';
|
|
||||||
export interface MenuSelectEventDetail {
|
export interface MenuSelectEventDetail {
|
||||||
item: SlMenuItem;
|
item: SlMenuItem;
|
||||||
}
|
}
|
||||||
@@ -29,13 +29,12 @@ export default class SlMenu extends ShoelaceElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handleClick(event: MouseEvent) {
|
private handleClick(event: MouseEvent) {
|
||||||
const target = event.target as HTMLElement;
|
if (!(event.target instanceof SlMenuItem)) {
|
||||||
const item = target.closest('sl-menu-item');
|
|
||||||
|
|
||||||
if (!item || item.disabled || item.inert) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const item: SlMenuItem = event.target;
|
||||||
|
|
||||||
if (item.type === 'checkbox') {
|
if (item.type === 'checkbox') {
|
||||||
item.checked = !item.checked;
|
item.checked = !item.checked;
|
||||||
}
|
}
|
||||||
@@ -48,19 +47,21 @@ export default class SlMenu extends ShoelaceElement {
|
|||||||
if (event.key === 'Enter' || event.key === ' ') {
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
const item = this.getCurrentItem();
|
const item = this.getCurrentItem();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
// Simulate a click to support @click handlers on menu items that also work with the keyboard
|
// Simulate a click to support @click handlers on menu items that also work with the keyboard
|
||||||
item?.click();
|
item?.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move the selection when pressing down or up
|
// Move the selection when pressing down or up
|
||||||
if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {
|
else if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {
|
||||||
const items = this.getAllItems();
|
const items = this.getAllItems();
|
||||||
const activeItem = this.getCurrentItem();
|
const activeItem = this.getCurrentItem();
|
||||||
let index = activeItem ? items.indexOf(activeItem) : 0;
|
let index = activeItem ? items.indexOf(activeItem) : 0;
|
||||||
|
|
||||||
if (items.length > 0) {
|
if (items.length > 0) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
if (event.key === 'ArrowDown') {
|
if (event.key === 'ArrowDown') {
|
||||||
index++;
|
index++;
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { expect, fixture } from '@open-wc/testing';
|
|||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
import { sendKeys } from '@web/test-runner-commands';
|
import { sendKeys } from '@web/test-runner-commands';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
import type { SlSelectEvent } from '../../events/sl-select';
|
||||||
import type SlMenu from './menu';
|
import type SlMenu from './menu';
|
||||||
import type SlSelectEvent from '../../events/sl-select';
|
|
||||||
|
|
||||||
describe('<sl-menu>', () => {
|
describe('<sl-menu>', () => {
|
||||||
it('emits sl-select with the correct event detail when clicking an item', async () => {
|
it('emits sl-select with the correct event detail when clicking an item', async () => {
|
||||||
|
|||||||
@@ -20,4 +20,26 @@ describe('<sl-radio-button>', () => {
|
|||||||
expect(radio1.checked).to.be.true;
|
expect(radio1.checked).to.be.true;
|
||||||
expect(radio2.checked).to.be.false;
|
expect(radio2.checked).to.be.false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should receive positional classes from <sl-button-group>', async () => {
|
||||||
|
const radioGroup = await fixture<SlRadioGroup>(html`
|
||||||
|
<sl-radio-group value="1">
|
||||||
|
<sl-radio-button id="radio-1" value="1"></sl-radio-button>
|
||||||
|
<sl-radio-button id="radio-2" value="2"></sl-radio-button>
|
||||||
|
<sl-radio-button id="radio-3" value="3"></sl-radio-button>
|
||||||
|
</sl-radio-group>
|
||||||
|
`);
|
||||||
|
const radio1 = radioGroup.querySelector<SlRadioButton>('#radio-1')!;
|
||||||
|
const radio2 = radioGroup.querySelector<SlRadioButton>('#radio-2')!;
|
||||||
|
const radio3 = radioGroup.querySelector<SlRadioButton>('#radio-3')!;
|
||||||
|
|
||||||
|
await Promise.all([radioGroup.updateComplete, radio1.updateComplete, radio2.updateComplete, radio3.updateComplete]);
|
||||||
|
|
||||||
|
expect(radio1.classList.contains('sl-button-group__button')).to.be.true;
|
||||||
|
expect(radio1.classList.contains('sl-button-group__button--first')).to.be.true;
|
||||||
|
expect(radio2.classList.contains('sl-button-group__button')).to.be.true;
|
||||||
|
expect(radio2.classList.contains('sl-button-group__button--inner')).to.be.true;
|
||||||
|
expect(radio3.classList.contains('sl-button-group__button')).to.be.true;
|
||||||
|
expect(radio3.classList.contains('sl-button-group__button--last')).to.be.true;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -327,11 +327,8 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
|
|||||||
const hasHelpTextSlot = this.hasSlotController.test('help-text');
|
const hasHelpTextSlot = this.hasSlotController.test('help-text');
|
||||||
const hasLabel = this.label ? true : !!hasLabelSlot;
|
const hasLabel = this.label ? true : !!hasLabelSlot;
|
||||||
const hasHelpText = this.helpText ? true : !!hasHelpTextSlot;
|
const hasHelpText = this.helpText ? true : !!hasHelpTextSlot;
|
||||||
|
|
||||||
const defaultSlot = html`
|
const defaultSlot = html`
|
||||||
<span @click=${this.handleRadioClick} @keydown=${this.handleKeyDown} role="presentation">
|
<slot @slotchange=${this.syncRadios} @click=${this.handleRadioClick} @keydown=${this.handleKeyDown}></slot>
|
||||||
<slot @slotchange=${this.syncRadios}></slot>
|
|
||||||
</span>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
@@ -378,7 +375,7 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
|
|||||||
|
|
||||||
${this.hasButtonGroup
|
${this.hasButtonGroup
|
||||||
? html`
|
? html`
|
||||||
<sl-button-group part="button-group" exportparts="base:button-group__base">
|
<sl-button-group part="button-group" exportparts="base:button-group__base" role="presentation">
|
||||||
${defaultSlot}
|
${defaultSlot}
|
||||||
</sl-button-group>
|
</sl-button-group>
|
||||||
`
|
`
|
||||||
@@ -395,6 +392,5 @@ export default class SlRadioGroup extends ShoelaceElement implements ShoelaceFor
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
`;
|
`;
|
||||||
/* eslint-enable lit-a11y/click-events-have-key-events */
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { clickOnElement } from '../../internal/test.js';
|
|||||||
import { runFormControlBaseTests } from '../../internal/test/form-control-base-tests.js';
|
import { runFormControlBaseTests } from '../../internal/test/form-control-base-tests.js';
|
||||||
import { sendKeys } from '@web/test-runner-commands';
|
import { sendKeys } from '@web/test-runner-commands';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import type SlChangeEvent from '../../events/sl-change.js';
|
import type { SlChangeEvent } from '../../events/sl-change.js';
|
||||||
import type SlRadio from '../radio/radio.js';
|
import type SlRadio from '../radio/radio.js';
|
||||||
import type SlRadioGroup from './radio-group.js';
|
import type SlRadioGroup from './radio-group.js';
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { html } from 'lit';
|
|||||||
import { LocalizeController } from '../../utilities/localize.js';
|
import { LocalizeController } from '../../utilities/localize.js';
|
||||||
import { property, query, state } from 'lit/decorators.js';
|
import { property, query, state } from 'lit/decorators.js';
|
||||||
import { scrollIntoView } from '../../internal/scroll.js';
|
import { scrollIntoView } from '../../internal/scroll.js';
|
||||||
|
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
||||||
import { waitForEvent } from '../../internal/event.js';
|
import { waitForEvent } from '../../internal/event.js';
|
||||||
import { watch } from '../../internal/watch.js';
|
import { watch } from '../../internal/watch.js';
|
||||||
import ShoelaceElement from '../../internal/shoelace-element.js';
|
import ShoelaceElement from '../../internal/shoelace-element.js';
|
||||||
@@ -15,10 +16,10 @@ import SlIcon from '../icon/icon.component.js';
|
|||||||
import SlPopup from '../popup/popup.component.js';
|
import SlPopup from '../popup/popup.component.js';
|
||||||
import SlTag from '../tag/tag.component.js';
|
import SlTag from '../tag/tag.component.js';
|
||||||
import styles from './select.styles.js';
|
import styles from './select.styles.js';
|
||||||
import type { CSSResultGroup } from 'lit';
|
import type { CSSResultGroup, TemplateResult } from 'lit';
|
||||||
import type { ShoelaceFormControl } from '../../internal/shoelace-element.js';
|
import type { ShoelaceFormControl } from '../../internal/shoelace-element.js';
|
||||||
|
import type { SlRemoveEvent } from '../../events/sl-remove.js';
|
||||||
import type SlOption from '../option/option.component.js';
|
import type SlOption from '../option/option.component.js';
|
||||||
import type SlRemoveEvent from '../../events/sl-remove.js';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @summary Selects allow you to choose items from a menu of predefined options.
|
* @summary Selects allow you to choose items from a menu of predefined options.
|
||||||
@@ -172,6 +173,31 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
|||||||
/** The select's required attribute. */
|
/** The select's required attribute. */
|
||||||
@property({ type: Boolean, reflect: true }) required = false;
|
@property({ type: Boolean, reflect: true }) required = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that customizes the tags to be rendered when multiple=true. The first argument is the option, the second
|
||||||
|
* is the current tag's index. The function should return either a Lit TemplateResult or a string containing trusted HTML of the symbol to render at
|
||||||
|
* the specified value.
|
||||||
|
*/
|
||||||
|
@property() getTag: (option: SlOption, index: number) => TemplateResult | string | HTMLElement = option => {
|
||||||
|
return html`
|
||||||
|
<sl-tag
|
||||||
|
part="tag"
|
||||||
|
exportparts="
|
||||||
|
base:tag__base,
|
||||||
|
content:tag__content,
|
||||||
|
remove-button:tag__remove-button,
|
||||||
|
remove-button__base:tag__remove-button__base
|
||||||
|
"
|
||||||
|
?pill=${this.pill}
|
||||||
|
size=${this.size}
|
||||||
|
removable
|
||||||
|
@sl-remove=${(event: SlRemoveEvent) => this.handleTagRemove(event, option)}
|
||||||
|
>
|
||||||
|
${option.getTextLabel()}
|
||||||
|
</sl-tag>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
/** Gets the validity state object */
|
/** Gets the validity state object */
|
||||||
get validity() {
|
get validity() {
|
||||||
return this.valueInput.validity;
|
return this.valueInput.validity;
|
||||||
@@ -547,6 +573,21 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
|||||||
this.formControlController.updateValidity();
|
this.formControlController.updateValidity();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
protected get tags() {
|
||||||
|
return this.selectedOptions.map((option, index) => {
|
||||||
|
if (index < this.maxOptionsVisible || this.maxOptionsVisible <= 0) {
|
||||||
|
const tag = this.getTag(option, index);
|
||||||
|
// Wrap so we can handle the remove
|
||||||
|
return html`<div @sl-remove=${(e: SlRemoveEvent) => this.handleTagRemove(e, option)}>
|
||||||
|
${typeof tag === 'string' ? unsafeHTML(tag) : tag}
|
||||||
|
</div>`;
|
||||||
|
} else if (index === this.maxOptionsVisible) {
|
||||||
|
// Hit tag limit
|
||||||
|
return html`<sl-tag>+${this.selectedOptions.length - index}</sl-tag>`;
|
||||||
|
}
|
||||||
|
return html``;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private handleInvalid(event: Event) {
|
private handleInvalid(event: Event) {
|
||||||
this.formControlController.setValidity(false);
|
this.formControlController.setValidity(false);
|
||||||
@@ -755,37 +796,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
|
|||||||
@blur=${this.handleBlur}
|
@blur=${this.handleBlur}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
${this.multiple
|
${this.multiple ? html`<div part="tags" class="select__tags">${this.tags}</div>` : ''}
|
||||||
? html`
|
|
||||||
<div part="tags" class="select__tags">
|
|
||||||
${this.selectedOptions.map((option, index) => {
|
|
||||||
if (index < this.maxOptionsVisible || this.maxOptionsVisible <= 0) {
|
|
||||||
return html`
|
|
||||||
<sl-tag
|
|
||||||
part="tag"
|
|
||||||
exportparts="
|
|
||||||
base:tag__base,
|
|
||||||
content:tag__content,
|
|
||||||
remove-button:tag__remove-button,
|
|
||||||
remove-button__base:tag__remove-button__base
|
|
||||||
"
|
|
||||||
?pill=${this.pill}
|
|
||||||
size=${this.size}
|
|
||||||
removable
|
|
||||||
@sl-remove=${(event: SlRemoveEvent) => this.handleTagRemove(event, option)}
|
|
||||||
>
|
|
||||||
${option.getTextLabel()}
|
|
||||||
</sl-tag>
|
|
||||||
`;
|
|
||||||
} else if (index === this.maxOptionsVisible) {
|
|
||||||
return html` <sl-tag size=${this.size}> +${this.selectedOptions.length - index} </sl-tag> `;
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ''}
|
|
||||||
|
|
||||||
<input
|
<input
|
||||||
class="select__value-input"
|
class="select__value-input"
|
||||||
|
|||||||
@@ -215,7 +215,9 @@ export default class SlSwitch extends ShoelaceElement implements ShoelaceFormCon
|
|||||||
<span part="thumb" class="switch__thumb"></span>
|
<span part="thumb" class="switch__thumb"></span>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<slot part="label" class="switch__label"></slot>
|
<div part="label" class="switch__label">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import { queryByTestId } from '../../internal/test/data-testid-helpers.js';
|
|||||||
import { sendKeys } from '@web/test-runner-commands';
|
import { sendKeys } from '@web/test-runner-commands';
|
||||||
import { waitForScrollingToEnd } from '../../internal/test/wait-for-scrolling.js';
|
import { waitForScrollingToEnd } from '../../internal/test/wait-for-scrolling.js';
|
||||||
import type { HTMLTemplateResult } from 'lit';
|
import type { HTMLTemplateResult } from 'lit';
|
||||||
|
import type { SlTabShowEvent } from '../../events/sl-tab-show.js';
|
||||||
import type SlTab from '../tab/tab.js';
|
import type SlTab from '../tab/tab.js';
|
||||||
import type SlTabGroup from './tab-group.js';
|
import type SlTabGroup from './tab-group.js';
|
||||||
import type SlTabPanel from '../tab-panel/tab-panel.js';
|
import type SlTabPanel from '../tab-panel/tab-panel.js';
|
||||||
import type SlTabShowEvent from '../../events/sl-tab-show.js';
|
|
||||||
|
|
||||||
interface ClientRectangles {
|
interface ClientRectangles {
|
||||||
body?: DOMRect;
|
body?: DOMRect;
|
||||||
|
|||||||
@@ -168,20 +168,6 @@ export default class SlTree extends ShoelaceElement {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private syncTreeItems(selectedItem: SlTreeItem) {
|
|
||||||
const items = this.getAllTreeItems();
|
|
||||||
|
|
||||||
if (this.selection === 'multiple') {
|
|
||||||
syncCheckboxes(selectedItem);
|
|
||||||
} else {
|
|
||||||
for (const item of items) {
|
|
||||||
if (item !== selectedItem) {
|
|
||||||
item.selected = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private selectItem(selectedItem: SlTreeItem) {
|
private selectItem(selectedItem: SlTreeItem) {
|
||||||
const previousSelection = [...this.selectedItems];
|
const previousSelection = [...this.selectedItems];
|
||||||
|
|
||||||
@@ -190,12 +176,12 @@ export default class SlTree extends ShoelaceElement {
|
|||||||
if (selectedItem.lazy) {
|
if (selectedItem.lazy) {
|
||||||
selectedItem.expanded = true;
|
selectedItem.expanded = true;
|
||||||
}
|
}
|
||||||
this.syncTreeItems(selectedItem);
|
syncCheckboxes(selectedItem);
|
||||||
} else if (this.selection === 'single' || selectedItem.isLeaf) {
|
} else if (this.selection === 'single' || selectedItem.isLeaf) {
|
||||||
selectedItem.expanded = !selectedItem.expanded;
|
const items = this.getAllTreeItems();
|
||||||
selectedItem.selected = true;
|
for (const item of items) {
|
||||||
|
item.selected = item === selectedItem;
|
||||||
this.syncTreeItems(selectedItem);
|
}
|
||||||
} else if (this.selection === 'leaf') {
|
} else if (this.selection === 'leaf') {
|
||||||
selectedItem.expanded = !selectedItem.expanded;
|
selectedItem.expanded = !selectedItem.expanded;
|
||||||
}
|
}
|
||||||
@@ -311,7 +297,7 @@ export default class SlTree extends ShoelaceElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.selection === 'multiple' && isExpandButton) {
|
if (isExpandButton) {
|
||||||
treeItem.expanded = !treeItem.expanded;
|
treeItem.expanded = !treeItem.expanded;
|
||||||
} else {
|
} else {
|
||||||
this.selectItem(treeItem);
|
this.selectItem(treeItem);
|
||||||
|
|||||||
@@ -275,7 +275,6 @@ describe('<sl-tree>', () => {
|
|||||||
// Assert
|
// Assert
|
||||||
expect(el.selectedItems.length).to.eq(1);
|
expect(el.selectedItems.length).to.eq(1);
|
||||||
expect(el.children[2]).to.have.attribute('selected');
|
expect(el.children[2]).to.have.attribute('selected');
|
||||||
expect(el.children[2]).to.have.attribute('expanded');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -439,7 +438,6 @@ describe('<sl-tree>', () => {
|
|||||||
await el.updateComplete;
|
await el.updateComplete;
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
expect(node).to.have.attribute('selected');
|
|
||||||
expect(node).to.have.attribute('expanded');
|
expect(node).to.have.attribute('expanded');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,35 +1,35 @@
|
|||||||
export type { default as SlAfterCollapseEvent } from './sl-after-collapse';
|
export type { SlAfterCollapseEvent } from './sl-after-collapse';
|
||||||
export type { default as SlAfterExpandEvent } from './sl-after-expand';
|
export type { SlAfterExpandEvent } from './sl-after-expand';
|
||||||
export type { default as SlAfterHideEvent } from './sl-after-hide';
|
export type { SlAfterHideEvent } from './sl-after-hide';
|
||||||
export type { default as SlAfterShowEvent } from './sl-after-show';
|
export type { SlAfterShowEvent } from './sl-after-show';
|
||||||
export type { default as SlBlurEvent } from './sl-blur';
|
export type { SlBlurEvent } from './sl-blur';
|
||||||
export type { default as SlCancelEvent } from './sl-cancel';
|
export type { SlCancelEvent } from './sl-cancel';
|
||||||
export type { default as SlChangeEvent } from './sl-change';
|
export type { SlChangeEvent } from './sl-change';
|
||||||
export type { default as SlClearEvent } from './sl-clear';
|
export type { SlClearEvent } from './sl-clear';
|
||||||
export type { default as SlCloseEvent } from './sl-close';
|
export type { SlCloseEvent } from './sl-close';
|
||||||
export type { default as SlCollapseEvent } from './sl-collapse';
|
export type { SlCollapseEvent } from './sl-collapse';
|
||||||
export type { default as SlCopyEvent } from './sl-copy';
|
export type { SlCopyEvent } from './sl-copy';
|
||||||
export type { default as SlErrorEvent } from './sl-error';
|
export type { SlErrorEvent } from './sl-error';
|
||||||
export type { default as SlExpandEvent } from './sl-expand';
|
export type { SlExpandEvent } from './sl-expand';
|
||||||
export type { default as SlFinishEvent } from './sl-finish';
|
export type { SlFinishEvent } from './sl-finish';
|
||||||
export type { default as SlFocusEvent } from './sl-focus';
|
export type { SlFocusEvent } from './sl-focus';
|
||||||
export type { default as SlHideEvent } from './sl-hide';
|
export type { SlHideEvent } from './sl-hide';
|
||||||
export type { default as SlHoverEvent } from './sl-hover';
|
export type { SlHoverEvent } from './sl-hover';
|
||||||
export type { default as SlInitialFocusEvent } from './sl-initial-focus';
|
export type { SlInitialFocusEvent } from './sl-initial-focus';
|
||||||
export type { default as SlInputEvent } from './sl-input';
|
export type { SlInputEvent } from './sl-input';
|
||||||
export type { default as SlInvalidEvent } from './sl-invalid';
|
export type { SlInvalidEvent } from './sl-invalid';
|
||||||
export type { default as SlLazyChangeEvent } from './sl-lazy-change';
|
export type { SlLazyChangeEvent } from './sl-lazy-change';
|
||||||
export type { default as SlLazyLoadEvent } from './sl-lazy-load';
|
export type { SlLazyLoadEvent } from './sl-lazy-load';
|
||||||
export type { default as SlLoadEvent } from './sl-load';
|
export type { SlLoadEvent } from './sl-load';
|
||||||
export type { default as SlMutationEvent } from './sl-mutation';
|
export type { SlMutationEvent } from './sl-mutation';
|
||||||
export type { default as SlRemoveEvent } from './sl-remove';
|
export type { SlRemoveEvent } from './sl-remove';
|
||||||
export type { default as SlRepositionEvent } from './sl-reposition';
|
export type { SlRepositionEvent } from './sl-reposition';
|
||||||
export type { default as SlRequestCloseEvent } from './sl-request-close';
|
export type { SlRequestCloseEvent } from './sl-request-close';
|
||||||
export type { default as SlResizeEvent } from './sl-resize';
|
export type { SlResizeEvent } from './sl-resize';
|
||||||
export type { default as SlSelectEvent } from './sl-select';
|
export type { SlSelectEvent } from './sl-select';
|
||||||
export type { default as SlSelectionChangeEvent } from './sl-selection-change';
|
export type { SlSelectionChangeEvent } from './sl-selection-change';
|
||||||
export type { default as SlShowEvent } from './sl-show';
|
export type { SlShowEvent } from './sl-show';
|
||||||
export type { default as SlSlideChangeEvent } from './sl-slide-change';
|
export type { SlSlideChangeEvent } from './sl-slide-change';
|
||||||
export type { default as SlStartEvent } from './sl-start';
|
export type { SlStartEvent } from './sl-start';
|
||||||
export type { default as SlTabHideEvent } from './sl-tab-hide';
|
export type { SlTabHideEvent } from './sl-tab-hide';
|
||||||
export type { default as SlTabShowEvent } from './sl-tab-show';
|
export type { SlTabShowEvent } from './sl-tab-show';
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlAfterCollapseEvent = CustomEvent<Record<PropertyKey, never>>;
|
export type SlAfterCollapseEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-after-collapse': SlAfterCollapseEvent;
|
'sl-after-collapse': SlAfterCollapseEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlAfterCollapseEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlAfterExpandEvent = CustomEvent<Record<PropertyKey, never>>;
|
export type SlAfterExpandEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-after-expand': SlAfterExpandEvent;
|
'sl-after-expand': SlAfterExpandEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlAfterExpandEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlAfterHideEvent = CustomEvent<Record<PropertyKey, never>>;
|
export type SlAfterHideEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-after-hide': SlAfterHideEvent;
|
'sl-after-hide': SlAfterHideEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlAfterHideEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlAfterShowEvent = CustomEvent<Record<PropertyKey, never>>;
|
export type SlAfterShowEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-after-show': SlAfterShowEvent;
|
'sl-after-show': SlAfterShowEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlAfterShowEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlBlurEvent = CustomEvent<Record<PropertyKey, never>>;
|
export type SlBlurEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-blur': SlBlurEvent;
|
'sl-blur': SlBlurEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlBlurEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlCancelEvent = CustomEvent<Record<PropertyKey, never>>;
|
export type SlCancelEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-cancel': SlCancelEvent;
|
'sl-cancel': SlCancelEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlCancelEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlChangeEvent = CustomEvent<Record<PropertyKey, never>>;
|
export type SlChangeEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-change': SlChangeEvent;
|
'sl-change': SlChangeEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlChangeEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlClearEvent = CustomEvent<Record<PropertyKey, never>>;
|
export type SlClearEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-clear': SlClearEvent;
|
'sl-clear': SlClearEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlClearEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlCloseEvent = CustomEvent<Record<PropertyKey, never>>;
|
export type SlCloseEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-close': SlCloseEvent;
|
'sl-close': SlCloseEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlCloseEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlCollapseEvent = CustomEvent<Record<PropertyKey, never>>;
|
export type SlCollapseEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-collapse': SlCollapseEvent;
|
'sl-collapse': SlCollapseEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlCollapseEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlCopyEvent = CustomEvent<{ value: string }>;
|
export type SlCopyEvent = CustomEvent<{ value: string }>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-copy': SlCopyEvent;
|
'sl-copy': SlCopyEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlCopyEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlErrorEvent = CustomEvent<{ status?: number }>;
|
export type SlErrorEvent = CustomEvent<{ status?: number }>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-error': SlErrorEvent;
|
'sl-error': SlErrorEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlErrorEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlExpandEvent = CustomEvent<Record<PropertyKey, never>>;
|
export type SlExpandEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-expand': SlExpandEvent;
|
'sl-expand': SlExpandEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlExpandEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlFinishEvent = CustomEvent<Record<PropertyKey, never>>;
|
export type SlFinishEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-finish': SlFinishEvent;
|
'sl-finish': SlFinishEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlFinishEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlFocusEvent = CustomEvent<Record<PropertyKey, never>>;
|
export type SlFocusEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-focus': SlFocusEvent;
|
'sl-focus': SlFocusEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlFocusEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlHideEvent = CustomEvent<Record<PropertyKey, never>>;
|
export type SlHideEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-hide': SlHideEvent;
|
'sl-hide': SlHideEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlHideEvent;
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
type SlHoverEvent = CustomEvent<{
|
export type SlHoverEvent = CustomEvent<{
|
||||||
phase: 'start' | 'move' | 'end';
|
phase: 'start' | 'move' | 'end';
|
||||||
value: number;
|
value: number;
|
||||||
}>;
|
}>;
|
||||||
@@ -8,5 +8,3 @@ declare global {
|
|||||||
'sl-hover': SlHoverEvent;
|
'sl-hover': SlHoverEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlHoverEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlInitialFocusEvent = CustomEvent<Record<PropertyKey, never>>;
|
export type SlInitialFocusEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-initial-focus': SlInitialFocusEvent;
|
'sl-initial-focus': SlInitialFocusEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlInitialFocusEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlInputEvent = CustomEvent<Record<PropertyKey, never>>;
|
export type SlInputEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-input': SlInputEvent;
|
'sl-input': SlInputEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlInputEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlInvalidEvent = CustomEvent<Record<PropertyKey, never>>;
|
export type SlInvalidEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-invalid': SlInvalidEvent;
|
'sl-invalid': SlInvalidEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlInvalidEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlLazyChangeEvent = CustomEvent<Record<PropertyKey, never>>;
|
export type SlLazyChangeEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-lazy-change': SlLazyChangeEvent;
|
'sl-lazy-change': SlLazyChangeEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlLazyChangeEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlLazyLoadEvent = CustomEvent<Record<PropertyKey, never>>;
|
export type SlLazyLoadEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-lazy-load': SlLazyLoadEvent;
|
'sl-lazy-load': SlLazyLoadEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlLazyLoadEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlLoadEvent = CustomEvent<Record<PropertyKey, never>>;
|
export type SlLoadEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-load': SlLoadEvent;
|
'sl-load': SlLoadEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlLoadEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlMutationEvent = CustomEvent<{ mutationList: MutationRecord[] }>;
|
export type SlMutationEvent = CustomEvent<{ mutationList: MutationRecord[] }>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-mutation': SlMutationEvent;
|
'sl-mutation': SlMutationEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlMutationEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlRemoveEvent = CustomEvent<Record<PropertyKey, never>>;
|
export type SlRemoveEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-remove': SlRemoveEvent;
|
'sl-remove': SlRemoveEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlRemoveEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlRepositionEvent = CustomEvent<Record<PropertyKey, never>>;
|
export type SlRepositionEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-reposition': SlRepositionEvent;
|
'sl-reposition': SlRepositionEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlRepositionEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlRequestCloseEvent = CustomEvent<{ source: 'close-button' | 'keyboard' | 'overlay' }>;
|
export type SlRequestCloseEvent = CustomEvent<{ source: 'close-button' | 'keyboard' | 'overlay' }>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-request-close': SlRequestCloseEvent;
|
'sl-request-close': SlRequestCloseEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlRequestCloseEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlResizeEvent = CustomEvent<{ entries: ResizeObserverEntry[] }>;
|
export type SlResizeEvent = CustomEvent<{ entries: ResizeObserverEntry[] }>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-resize': SlResizeEvent;
|
'sl-resize': SlResizeEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlResizeEvent;
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import type SlMenuItem from '../components/menu-item/menu-item';
|
import type SlMenuItem from '../components/menu-item/menu-item';
|
||||||
|
|
||||||
type SlSelectEvent = CustomEvent<{ item: SlMenuItem }>;
|
export type SlSelectEvent = CustomEvent<{ item: SlMenuItem }>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-select': SlSelectEvent;
|
'sl-select': SlSelectEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlSelectEvent;
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import type SlTreeItem from '../components/tree-item/tree-item';
|
import type SlTreeItem from '../components/tree-item/tree-item';
|
||||||
|
|
||||||
type SlSelectionChangeEvent = CustomEvent<{ selection: SlTreeItem[] }>;
|
export type SlSelectionChangeEvent = CustomEvent<{ selection: SlTreeItem[] }>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-selection-change': SlSelectionChangeEvent;
|
'sl-selection-change': SlSelectionChangeEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlSelectionChangeEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlShowEvent = CustomEvent<Record<PropertyKey, never>>;
|
export type SlShowEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-show': SlShowEvent;
|
'sl-show': SlShowEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlShowEvent;
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import type SlCarouselItem from '../components/carousel-item/carousel-item';
|
import type SlCarouselItem from '../components/carousel-item/carousel-item';
|
||||||
|
|
||||||
type SlSlideChangeEvent = CustomEvent<{ index: number; slide: SlCarouselItem }>;
|
export type SlSlideChangeEvent = CustomEvent<{ index: number; slide: SlCarouselItem }>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-slide-change': SlSlideChangeEvent;
|
'sl-slide-change': SlSlideChangeEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlSlideChangeEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlStartEvent = CustomEvent<Record<PropertyKey, never>>;
|
export type SlStartEvent = CustomEvent<Record<PropertyKey, never>>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-start': SlStartEvent;
|
'sl-start': SlStartEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlStartEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlTabHideEvent = CustomEvent<{ name: string }>;
|
export type SlTabHideEvent = CustomEvent<{ name: string }>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-tab-hide': SlTabHideEvent;
|
'sl-tab-hide': SlTabHideEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlTabHideEvent;
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
type SlTabShowEvent = CustomEvent<{ name: string }>;
|
export type SlTabShowEvent = CustomEvent<{ name: string }>;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface GlobalEventHandlersEventMap {
|
interface GlobalEventHandlersEventMap {
|
||||||
'sl-tab-show': SlTabShowEvent;
|
'sl-tab-show': SlTabShowEvent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SlTabShowEvent;
|
|
||||||
|
|||||||
22
src/internal/active-elements.ts
Normal file
22
src/internal/active-elements.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Use a generator so we can iterate and possibly break early.
|
||||||
|
* @example
|
||||||
|
* // to operate like a regular array. This kinda nullifies generator benefits, but worth knowing if you need the whole array.
|
||||||
|
* const allActiveElements = [...activeElements()]
|
||||||
|
*
|
||||||
|
* // Early return
|
||||||
|
* for (const activeElement of activeElements()) {
|
||||||
|
* if (<cond>) {
|
||||||
|
* break; // Break the loop, dont need to iterate over the whole array or store an array in memory!
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export function* activeElements(activeElement: Element | null = document.activeElement): Generator<Element> {
|
||||||
|
if (activeElement === null || activeElement === undefined) return;
|
||||||
|
|
||||||
|
yield activeElement;
|
||||||
|
|
||||||
|
if ('shadowRoot' in activeElement && activeElement.shadowRoot && activeElement.shadowRoot.mode !== 'closed') {
|
||||||
|
yield* activeElements(activeElement.shadowRoot.activeElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -182,7 +182,11 @@ export class FormControlController implements ReactiveController {
|
|||||||
if (!disabled && !isButton && typeof name === 'string' && name.length > 0 && typeof value !== 'undefined') {
|
if (!disabled && !isButton && typeof name === 'string' && name.length > 0 && typeof value !== 'undefined') {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
(value as unknown[]).forEach(val => {
|
(value as unknown[]).forEach(val => {
|
||||||
event.formData.append(name, (val as string | number | boolean).toString());
|
if (val instanceof File) {
|
||||||
|
event.formData.append(name, val, val.name);
|
||||||
|
} else {
|
||||||
|
event.formData.append(name, (val as string | number | boolean).toString());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
event.formData.append(name, (value as string | number | boolean).toString());
|
event.formData.append(name, (value as string | number | boolean).toString());
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { activeElements } from './active-elements.js';
|
||||||
import { getTabbableElements } from './tabbable.js';
|
import { getTabbableElements } from './tabbable.js';
|
||||||
|
|
||||||
let activeModals: HTMLElement[] = [];
|
let activeModals: HTMLElement[] = [];
|
||||||
@@ -55,6 +56,20 @@ export default class Modal {
|
|||||||
return getTabbableElements(this.element).findIndex(el => el === this.currentFocus);
|
return getTabbableElements(this.element).findIndex(el => el === this.currentFocus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the `startElement` is already focused. This is important if the modal already
|
||||||
|
* has an existing focus prior to the first tab key.
|
||||||
|
*/
|
||||||
|
startElementAlreadyFocused(startElement: HTMLElement) {
|
||||||
|
for (const activeElement of activeElements()) {
|
||||||
|
if (startElement === activeElement) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
handleKeyDown = (event: KeyboardEvent) => {
|
handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (event.key !== 'Tab') return;
|
if (event.key !== 'Tab') return;
|
||||||
|
|
||||||
@@ -68,7 +83,10 @@ export default class Modal {
|
|||||||
|
|
||||||
const tabbableElements = getTabbableElements(this.element);
|
const tabbableElements = getTabbableElements(this.element);
|
||||||
const start = tabbableElements[0];
|
const start = tabbableElements[0];
|
||||||
let focusIndex = this.currentFocusIndex;
|
|
||||||
|
// Sometimes we programmatically focus the first element in a modal.
|
||||||
|
// Lets make sure the start element isn't already focused.
|
||||||
|
let focusIndex = this.startElementAlreadyFocused(start) ? 0 : this.currentFocusIndex;
|
||||||
|
|
||||||
if (focusIndex === -1) {
|
if (focusIndex === -1) {
|
||||||
this.currentFocus = start;
|
this.currentFocus = start;
|
||||||
|
|||||||
@@ -133,10 +133,10 @@ before(async () => {
|
|||||||
|
|
||||||
relevantMetadata.forEach(({ tagName, path }) => {
|
relevantMetadata.forEach(({ tagName, path }) => {
|
||||||
it(`Should not register any components: ${tagName}`, async () => {
|
it(`Should not register any components: ${tagName}`, async () => {
|
||||||
// Check if importing the files automatically registers any components
|
|
||||||
await import('../../dist/' + path);
|
await import('../../dist/' + path);
|
||||||
|
|
||||||
const registeredTags = tagNames.filter(tag => Boolean(window.customElements.get(tag)));
|
// Need to make sure we remove the current tag from the tagNames and *then* see whats been registered.
|
||||||
|
const registeredTags = tagNames.filter(tag => tag !== tagName && Boolean(window.customElements.get(tag)));
|
||||||
|
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
`Expected ${path} to not register any tags, but it registered the following tags: ` +
|
`Expected ${path} to not register any tags, but it registered the following tags: ` +
|
||||||
|
|||||||
147
src/internal/tabbable.test.ts
Normal file
147
src/internal/tabbable.test.ts
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { elementUpdated, expect, fixture } from '@open-wc/testing';
|
||||||
|
|
||||||
|
import '../../dist/shoelace.js';
|
||||||
|
import { activeElements } from './active-elements.js';
|
||||||
|
import { html } from 'lit';
|
||||||
|
import { sendKeys } from '@web/test-runner-commands';
|
||||||
|
|
||||||
|
async function holdShiftKey(callback: () => Promise<void>) {
|
||||||
|
await sendKeys({ down: 'Shift' });
|
||||||
|
await callback();
|
||||||
|
await sendKeys({ up: 'Shift' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabKey =
|
||||||
|
navigator.userAgent.includes('Safari') && !navigator.userAgent.includes('HeadlessChrome') ? 'Alt+Tab' : 'Tab';
|
||||||
|
|
||||||
|
// Simple helper to turn the activeElements generator into an array
|
||||||
|
function activeElementsArray() {
|
||||||
|
return [...activeElements()];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDeepestActiveElement() {
|
||||||
|
return activeElementsArray().pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.customElements.define(
|
||||||
|
'tab-test-1',
|
||||||
|
class extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
}
|
||||||
|
connectedCallback() {
|
||||||
|
this.shadowRoot!.innerHTML = `
|
||||||
|
<sl-drawer>
|
||||||
|
<slot name="label" slot="label"></slot>
|
||||||
|
|
||||||
|
<slot></slot>
|
||||||
|
|
||||||
|
<slot name="footer" slot="footer"></slot>
|
||||||
|
</sl-drawer>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it('Should allow tabbing to slotted elements', async () => {
|
||||||
|
const el = await fixture(html`
|
||||||
|
<tab-test-1>
|
||||||
|
<div slot="label">
|
||||||
|
<sl-button id="focus-1">Focus 1</sl-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<!-- Focus 2 lives as the close-button from <sl-drawer> -->
|
||||||
|
<sl-button id="focus-3">Focus 3</sl-button>
|
||||||
|
<button id="focus-4">Focus 4</sl-button>
|
||||||
|
<input id="focus-5" value="Focus 5">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div slot="footer">
|
||||||
|
<div id="focus-6" tabindex="0">Focus 6</div>
|
||||||
|
<button tabindex="-1">No Focus</button>
|
||||||
|
</div>
|
||||||
|
</tab-test-1>
|
||||||
|
`);
|
||||||
|
|
||||||
|
const drawer = el.shadowRoot?.querySelector('sl-drawer');
|
||||||
|
|
||||||
|
if (drawer === null || drawer === undefined) throw Error('Could not find drawer inside of the test element');
|
||||||
|
|
||||||
|
await drawer.show();
|
||||||
|
|
||||||
|
await elementUpdated(drawer);
|
||||||
|
|
||||||
|
const focusZero = drawer.shadowRoot?.querySelector("[role='dialog']");
|
||||||
|
|
||||||
|
if (focusZero === null || focusZero === undefined) throw Error('Could not find dialog panel inside <sl-drawer>');
|
||||||
|
|
||||||
|
const focusOne = el.querySelector('#focus-1');
|
||||||
|
const focusTwo = drawer.shadowRoot?.querySelector("[part~='close-button']");
|
||||||
|
|
||||||
|
if (focusTwo === null || focusTwo === undefined) throw Error('Could not find close button inside <sl-drawer>');
|
||||||
|
|
||||||
|
const focusThree = el.querySelector('#focus-3');
|
||||||
|
const focusFour = el.querySelector('#focus-4');
|
||||||
|
const focusFive = el.querySelector('#focus-5');
|
||||||
|
const focusSix = el.querySelector('#focus-6');
|
||||||
|
|
||||||
|
// When we open drawer, we should be focused on the panel to start.
|
||||||
|
expect(getDeepestActiveElement()).to.equal(focusZero);
|
||||||
|
|
||||||
|
await sendKeys({ press: tabKey });
|
||||||
|
expect(activeElementsArray()).to.include(focusOne);
|
||||||
|
|
||||||
|
// When we hit the <Tab> key we should go to the "close button" on the drawer
|
||||||
|
await sendKeys({ press: tabKey });
|
||||||
|
expect(activeElementsArray()).to.include(focusTwo);
|
||||||
|
|
||||||
|
await sendKeys({ press: tabKey });
|
||||||
|
expect(activeElementsArray()).to.include(focusThree);
|
||||||
|
|
||||||
|
await sendKeys({ press: tabKey });
|
||||||
|
expect(activeElementsArray()).to.include(focusFour);
|
||||||
|
|
||||||
|
await sendKeys({ press: tabKey });
|
||||||
|
expect(activeElementsArray()).to.include(focusFive);
|
||||||
|
|
||||||
|
await sendKeys({ press: tabKey });
|
||||||
|
expect(activeElementsArray()).to.include(focusSix);
|
||||||
|
|
||||||
|
// Now we should loop back to #panel
|
||||||
|
await sendKeys({ press: tabKey });
|
||||||
|
expect(activeElementsArray()).to.include(focusZero);
|
||||||
|
|
||||||
|
// Now we should loop back to #panel
|
||||||
|
await sendKeys({ press: tabKey });
|
||||||
|
expect(activeElementsArray()).to.include(focusOne);
|
||||||
|
|
||||||
|
// Let's reset and try from starting point 0 and go backwards.
|
||||||
|
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
|
||||||
|
expect(activeElementsArray()).to.include(focusZero);
|
||||||
|
|
||||||
|
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
|
||||||
|
expect(activeElementsArray()).to.include(focusSix);
|
||||||
|
|
||||||
|
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
|
||||||
|
expect(activeElementsArray()).to.include(focusFive);
|
||||||
|
|
||||||
|
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
|
||||||
|
expect(activeElementsArray()).to.include(focusFour);
|
||||||
|
|
||||||
|
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
|
||||||
|
expect(activeElementsArray()).to.include(focusThree);
|
||||||
|
|
||||||
|
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
|
||||||
|
expect(activeElementsArray()).to.include(focusTwo);
|
||||||
|
|
||||||
|
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
|
||||||
|
expect(activeElementsArray()).to.include(focusOne);
|
||||||
|
|
||||||
|
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
|
||||||
|
expect(activeElementsArray()).to.include(focusZero);
|
||||||
|
|
||||||
|
await holdShiftKey(async () => await sendKeys({ press: tabKey }));
|
||||||
|
expect(activeElementsArray()).to.include(focusSix);
|
||||||
|
});
|
||||||
@@ -69,11 +69,32 @@ export function getTabbableBoundary(root: HTMLElement | ShadowRoot) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getTabbableElements(root: HTMLElement | ShadowRoot) {
|
export function getTabbableElements(root: HTMLElement | ShadowRoot) {
|
||||||
const allElements: HTMLElement[] = [];
|
const tabbableElements: HTMLElement[] = [];
|
||||||
|
|
||||||
function walk(el: HTMLElement | ShadowRoot) {
|
function walk(el: HTMLElement | ShadowRoot) {
|
||||||
if (el instanceof Element) {
|
if (el instanceof Element) {
|
||||||
allElements.push(el);
|
// if the element has "inert" we can just no-op it.
|
||||||
|
if (el.hasAttribute('inert')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tabbableElements.includes(el) && isTabbable(el)) {
|
||||||
|
tabbableElements.push(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This looks funky. Basically a slot's children will always be picked up *if* they're within the `root` element.
|
||||||
|
* However, there is an edge case when, if the `root` is wrapped by another shadow DOM, it won't grab the children.
|
||||||
|
* This fixes that fun edge case.
|
||||||
|
*/
|
||||||
|
const slotChildrenOutsideRootElement = (slotElement: HTMLSlotElement) =>
|
||||||
|
(slotElement.getRootNode({ composed: true }) as ShadowRoot | null)?.host !== root;
|
||||||
|
|
||||||
|
if (el instanceof HTMLSlotElement && slotChildrenOutsideRootElement(el)) {
|
||||||
|
el.assignedElements({ flatten: true }).forEach((assignedEl: HTMLElement) => {
|
||||||
|
walk(assignedEl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (el.shadowRoot !== null && el.shadowRoot.mode === 'open') {
|
if (el.shadowRoot !== null && el.shadowRoot.mode === 'open') {
|
||||||
walk(el.shadowRoot);
|
walk(el.shadowRoot);
|
||||||
@@ -86,10 +107,14 @@ export function getTabbableElements(root: HTMLElement | ShadowRoot) {
|
|||||||
// Collect all elements including the root
|
// Collect all elements including the root
|
||||||
walk(root);
|
walk(root);
|
||||||
|
|
||||||
return allElements.filter(isTabbable).sort((a, b) => {
|
return tabbableElements;
|
||||||
// Make sure we sort by tabindex.
|
|
||||||
const aTabindex = Number(a.getAttribute('tabindex')) || 0;
|
// Is this worth having? Most sorts will always add increased overhead. And positive tabindexes shouldn't really be used.
|
||||||
const bTabindex = Number(b.getAttribute('tabindex')) || 0;
|
// So is it worth being right? Or fast?
|
||||||
return bTabindex - aTabindex;
|
// return tabbableElements.filter(isTabbable).sort((a, b) => {
|
||||||
});
|
// // Make sure we sort by tabindex.
|
||||||
|
// const aTabindex = Number(a.getAttribute('tabindex')) || 0;
|
||||||
|
// const bTabindex = Number(b.getAttribute('tabindex')) || 0;
|
||||||
|
// return bTabindex - aTabindex;
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export { default as SlDialog } from './components/dialog/dialog.js';
|
|||||||
export { default as SlDivider } from './components/divider/divider.js';
|
export { default as SlDivider } from './components/divider/divider.js';
|
||||||
export { default as SlDrawer } from './components/drawer/drawer.js';
|
export { default as SlDrawer } from './components/drawer/drawer.js';
|
||||||
export { default as SlDropdown } from './components/dropdown/dropdown.js';
|
export { default as SlDropdown } from './components/dropdown/dropdown.js';
|
||||||
|
export { default as SlFileInput } from './components/file-input/file-input.js';
|
||||||
export { default as SlFormatBytes } from './components/format-bytes/format-bytes.js';
|
export { default as SlFormatBytes } from './components/format-bytes/format-bytes.js';
|
||||||
export { default as SlFormatDate } from './components/format-date/format-date.js';
|
export { default as SlFormatDate } from './components/format-date/format-date.js';
|
||||||
export { default as SlFormatNumber } from './components/format-number/format-number.js';
|
export { default as SlFormatNumber } from './components/format-number/format-number.js';
|
||||||
|
|||||||
@@ -28,7 +28,8 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"useUnknownInCatchVariables": true,
|
"useUnknownInCatchVariables": true,
|
||||||
"baseUrl": ".",
|
"isolatedModules": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
"types": [
|
"types": [
|
||||||
"mocha",
|
"mocha",
|
||||||
"user-agent-data-types"
|
"user-agent-data-types"
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"extends": "./tsconfig",
|
"extends": "./tsconfig",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
|
||||||
"rootDir": "./src"
|
"rootDir": "./src"
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
|
|||||||
Reference in New Issue
Block a user