mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-20 07:54:14 +00:00
Compare commits
231 Commits
v2.0.0-bet
...
v2.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
deec097267 | ||
|
|
873e280700 | ||
|
|
5d047f7a93 | ||
|
|
f24ab23752 | ||
|
|
44ecc8ce56 | ||
|
|
e758b1d9bb | ||
|
|
5cdbaa873d | ||
|
|
e9aca6cedb | ||
|
|
7c3896ed42 | ||
|
|
93158e8e90 | ||
|
|
5f9bbdfa06 | ||
|
|
3a0f486e98 | ||
|
|
29c671c0f4 | ||
|
|
88c4bef5e7 | ||
|
|
6066bc468b | ||
|
|
efd944d822 | ||
|
|
4a3f2caf59 | ||
|
|
511182b41b | ||
|
|
1088a51ed5 | ||
|
|
e3e0842bdd | ||
|
|
e4c908b08b | ||
|
|
f86578a213 | ||
|
|
3c2f5ec48e | ||
|
|
fec7ef17aa | ||
|
|
29ff99dd76 | ||
|
|
6b9b410bdc | ||
|
|
b45a9d55ca | ||
|
|
7ce079b7a1 | ||
|
|
b0ba9ff14f | ||
|
|
f665bf984b | ||
|
|
ac429a62c0 | ||
|
|
dc909d10b6 | ||
|
|
aa65077b12 | ||
|
|
7e37c51856 | ||
|
|
6e26daf804 | ||
|
|
25c2d2d5bf | ||
|
|
edc9e69f30 | ||
|
|
2e7ac38678 | ||
|
|
1a68c825c0 | ||
|
|
2cbdeeade0 | ||
|
|
79624f63ed | ||
|
|
01a8ec36ec | ||
|
|
60324885ed | ||
|
|
0c2f43b837 | ||
|
|
0df27cf730 | ||
|
|
68ed69292c | ||
|
|
62c58b3a8c | ||
|
|
1fbb809057 | ||
|
|
e2d2f5d670 | ||
|
|
acef0da2c1 | ||
|
|
3c66d2ab99 | ||
|
|
31f16c4680 | ||
|
|
8056379fdd | ||
|
|
9a6b9a7841 | ||
|
|
02fc39ebe0 | ||
|
|
a90b22c05d | ||
|
|
f5dd4f2aca | ||
|
|
d5b3489b22 | ||
|
|
8ee5f19184 | ||
|
|
d0a32d48b1 | ||
|
|
bf527437a0 | ||
|
|
ae3070ac45 | ||
|
|
6af68343a7 | ||
|
|
e632b51eb8 | ||
|
|
c8e633c4a1 | ||
|
|
724f4a59db | ||
|
|
041364fb7d | ||
|
|
fbcb4d8dbd | ||
|
|
27a6b4a8c9 | ||
|
|
c814e9e94e | ||
|
|
0e957c0cd4 | ||
|
|
b330657e0a | ||
|
|
a0fce64fd9 | ||
|
|
b183a04fba | ||
|
|
7645b997b2 | ||
|
|
d81e2f1470 | ||
|
|
f50fe72df2 | ||
|
|
dcca64a986 | ||
|
|
c216cfe0fd | ||
|
|
192f15e3b7 | ||
|
|
01be3daf6d | ||
|
|
d36eec5637 | ||
|
|
121464fa2d | ||
|
|
ee0254e822 | ||
|
|
27f634402c | ||
|
|
67fbe3b34e | ||
|
|
164ebce990 | ||
|
|
571ae704e0 | ||
|
|
ad305fb653 | ||
|
|
fc0541ce53 | ||
|
|
c8555f448c | ||
|
|
96e41198ec | ||
|
|
57064aef4d | ||
|
|
cf200aa58a | ||
|
|
388a4f85a4 | ||
|
|
5f8556b1b2 | ||
|
|
e411b57124 | ||
|
|
0120e7429d | ||
|
|
377dbe28eb | ||
|
|
b25b1d5750 | ||
|
|
0e1b792bf7 | ||
|
|
417f0d17c9 | ||
|
|
d9252fe755 | ||
|
|
c5555ab5fe | ||
|
|
eb61dc7d91 | ||
|
|
a4c522f090 | ||
|
|
a8fe8c3e71 | ||
|
|
87000306c0 | ||
|
|
ae07b7d0a8 | ||
|
|
626b76610f | ||
|
|
b4a1e1b0c9 | ||
|
|
913243f8c1 | ||
|
|
563ed81984 | ||
|
|
fcbf339a86 | ||
|
|
70585e1d2a | ||
|
|
479e568296 | ||
|
|
a473e41ab3 | ||
|
|
92f6a2d8e9 | ||
|
|
06dc5740bf | ||
|
|
fe524e0fac | ||
|
|
ea7de2eb70 | ||
|
|
b8d02537a6 | ||
|
|
24744ef8c5 | ||
|
|
69997466be | ||
|
|
f7d7fdf5b1 | ||
|
|
c0013c5639 | ||
|
|
935040204f | ||
|
|
2ffbf9b017 | ||
|
|
0f67b9a9d1 | ||
|
|
c5dee51233 | ||
|
|
b07238d536 | ||
|
|
e2a65c28f4 | ||
|
|
1f457cdde0 | ||
|
|
46fda5f0a6 | ||
|
|
e22c2f839b | ||
|
|
5bff912162 | ||
|
|
f3010cecbe | ||
|
|
2dc275defd | ||
|
|
9f79445292 | ||
|
|
10cb26b81e | ||
|
|
3722e0ad91 | ||
|
|
f28a0ec743 | ||
|
|
a42b393bf1 | ||
|
|
41f50777bd | ||
|
|
6afc3ba12e | ||
|
|
d8b7040a9e | ||
|
|
59cd70ae6f | ||
|
|
6d9c8561fb | ||
|
|
edb8a92838 | ||
|
|
da3ffe9a60 | ||
|
|
e2b791dee8 | ||
|
|
9178be576b | ||
|
|
752f5cff55 | ||
|
|
3675787ddd | ||
|
|
6284ed0347 | ||
|
|
1ff05c4b3d | ||
|
|
066abe4e52 | ||
|
|
e9134ddcf6 | ||
|
|
0237851aa1 | ||
|
|
1841251fdf | ||
|
|
b1e48406f3 | ||
|
|
4ca51cf11e | ||
|
|
41bd61c3a5 | ||
|
|
c6df057e15 | ||
|
|
1428481a5c | ||
|
|
1c359fbea9 | ||
|
|
0329694760 | ||
|
|
936ec5a23a | ||
|
|
d291506284 | ||
|
|
3877351fdc | ||
|
|
7885572ebd | ||
|
|
2ee926023c | ||
|
|
69e557cd8c | ||
|
|
f2efa73e20 | ||
|
|
2dd57956d5 | ||
|
|
ce86a1c9f2 | ||
|
|
a0f83c3b2b | ||
|
|
80a16ee42a | ||
|
|
31b05fedd3 | ||
|
|
d3a7950483 | ||
|
|
f299621490 | ||
|
|
c62ee3e207 | ||
|
|
a313087937 | ||
|
|
3945571b20 | ||
|
|
c6a044416a | ||
|
|
d2f1b50ad0 | ||
|
|
1f1bae77dd | ||
|
|
829e22dc1e | ||
|
|
8b28ee818f | ||
|
|
afb2e3d5b4 | ||
|
|
fb0adb9ccb | ||
|
|
efea514f5a | ||
|
|
fda9bd52a3 | ||
|
|
01f8ce6b03 | ||
|
|
e10651565f | ||
|
|
35aa56d334 | ||
|
|
31e1f2fc59 | ||
|
|
ee30f7a10b | ||
|
|
a50909d474 | ||
|
|
63115d51e5 | ||
|
|
026036a14b | ||
|
|
22e09a778b | ||
|
|
488088d5f0 | ||
|
|
0c18880e5c | ||
|
|
0b0a571d17 | ||
|
|
ea9e596279 | ||
|
|
fe17c8406b | ||
|
|
e7a4a5135d | ||
|
|
ebf12860be | ||
|
|
4b81778e46 | ||
|
|
28a8a90bb1 | ||
|
|
8dab5a8f04 | ||
|
|
4ac9483213 | ||
|
|
ae3c0d72c0 | ||
|
|
83b73e823d | ||
|
|
da6ae608f1 | ||
|
|
1e39647d7f | ||
|
|
4dc92247d5 | ||
|
|
1b6f982d68 | ||
|
|
81c775ea87 | ||
|
|
09f741e930 | ||
|
|
602a863d89 | ||
|
|
3dd19a7f62 | ||
|
|
bdf890ab0d | ||
|
|
0b7eae0c20 | ||
|
|
f3e273744a | ||
|
|
7f9b0bd5a2 | ||
|
|
6579a95999 | ||
|
|
61738a3f6a | ||
|
|
9061c1987a | ||
|
|
48be3f46b8 |
@@ -1,7 +1,16 @@
|
||||
/* eslint-env node */
|
||||
|
||||
module.exports = {
|
||||
plugins: ['@typescript-eslint', 'wc', 'lit', 'lit-a11y', 'chai-expect', 'chai-friendly', 'import'],
|
||||
plugins: [
|
||||
'@typescript-eslint',
|
||||
'wc',
|
||||
'lit',
|
||||
'lit-a11y',
|
||||
'chai-expect',
|
||||
'chai-friendly',
|
||||
'import',
|
||||
'sort-imports-es6-autofix'
|
||||
],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:wc/recommended',
|
||||
@@ -13,7 +22,6 @@ module.exports = {
|
||||
es2021: true,
|
||||
browser: true
|
||||
},
|
||||
reportUnusedDisableDirectives: true,
|
||||
parserOptions: {
|
||||
sourceType: 'module'
|
||||
},
|
||||
@@ -151,7 +159,7 @@ module.exports = {
|
||||
'prefer-rest-params': 'warn',
|
||||
'prefer-spread': 'warn',
|
||||
'prefer-template': 'off',
|
||||
'no-else-return': 'warn',
|
||||
'no-else-return': 'off',
|
||||
'func-names': ['warn', 'never'],
|
||||
'one-var': ['warn', 'never'],
|
||||
'operator-assignment': 'warn',
|
||||
@@ -172,22 +180,12 @@ module.exports = {
|
||||
}
|
||||
],
|
||||
'import/no-duplicates': 'warn',
|
||||
'import/order': [
|
||||
'warn',
|
||||
'sort-imports-es6-autofix/sort-imports-es6': [
|
||||
2,
|
||||
{
|
||||
groups: ['builtin', 'external', 'internal', 'unknown', 'parent', 'sibling', 'index', 'object', 'type'],
|
||||
pathGroups: [
|
||||
{
|
||||
pattern: 'dist/**',
|
||||
group: 'external'
|
||||
}
|
||||
],
|
||||
alphabetize: {
|
||||
order: 'asc',
|
||||
caseInsensitive: true
|
||||
},
|
||||
'newlines-between': 'never',
|
||||
warnOnUnassignedImports: true
|
||||
ignoreCase: true,
|
||||
ignoreMemberSort: false,
|
||||
memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single']
|
||||
}
|
||||
],
|
||||
'wc/guard-super-call': 'off'
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"apos",
|
||||
"atrule",
|
||||
"autocorrect",
|
||||
"autofix",
|
||||
"autoplay",
|
||||
"bezier",
|
||||
"boxicons",
|
||||
@@ -20,6 +21,7 @@
|
||||
"codebases",
|
||||
"codepen",
|
||||
"colocated",
|
||||
"colour",
|
||||
"combobox",
|
||||
"Composability",
|
||||
"Consolas",
|
||||
@@ -44,6 +46,7 @@
|
||||
"fieldsets",
|
||||
"formaction",
|
||||
"formdata",
|
||||
"formenctype",
|
||||
"formmethod",
|
||||
"formnovalidate",
|
||||
"formtarget",
|
||||
@@ -120,6 +123,7 @@
|
||||
"tera",
|
||||
"textareas",
|
||||
"textfield",
|
||||
"tinycolor",
|
||||
"transitionend",
|
||||
"treeitem",
|
||||
"Triaging",
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import fs from 'fs';
|
||||
import { generateCustomData } from 'cem-plugin-vs-code-custom-data-generator';
|
||||
import commandLineArgs from 'command-line-args';
|
||||
import { parse } from 'comment-parser';
|
||||
import { pascalCase } from 'pascal-case';
|
||||
|
||||
const packageData = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
||||
const { name, description, version, author, homepage, license } = packageData;
|
||||
|
||||
const { outdir } = commandLineArgs([
|
||||
{ name: 'litelement', type: String },
|
||||
{ name: 'analyze', defaultOption: true },
|
||||
{ name: 'outdir', type: String }
|
||||
]);
|
||||
|
||||
function noDash(string) {
|
||||
return string.replace(/^\s?-/, '').trim();
|
||||
}
|
||||
@@ -37,7 +45,7 @@ export default {
|
||||
case ts.SyntaxKind.ClassDeclaration: {
|
||||
const className = node.name.getText();
|
||||
const classDoc = moduleDoc?.declarations?.find(declaration => declaration.name === className);
|
||||
const customTags = ['title', 'animation', 'dependency', 'since', 'status'];
|
||||
const customTags = ['animation', 'dependency', 'documentation', 'since', 'status', 'title'];
|
||||
let customComments = '/**';
|
||||
|
||||
node.jsDoc?.forEach(jsDoc => {
|
||||
@@ -73,6 +81,7 @@ export default {
|
||||
break;
|
||||
|
||||
// Value-only metadata tags
|
||||
case 'documentation':
|
||||
case 'since':
|
||||
case 'status':
|
||||
case 'title':
|
||||
@@ -148,6 +157,11 @@ export default {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
// Generate custom VS Code data
|
||||
generateCustomData({
|
||||
outdir,
|
||||
cssFileName: null
|
||||
})
|
||||
]
|
||||
};
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
- [Menu](/components/menu)
|
||||
- [Menu Item](/components/menu-item)
|
||||
- [Menu Label](/components/menu-label)
|
||||
- [Option](/components/option)
|
||||
- [Progress Bar](/components/progress-bar)
|
||||
- [Progress Ring](/components/progress-ring)
|
||||
- [QR Code](/components/qr-code)
|
||||
@@ -91,6 +92,7 @@
|
||||
- [Border Radius](/tokens/border-radius)
|
||||
- [Transition](/tokens/transition)
|
||||
- [Z-index](/tokens/z-index)
|
||||
- [More](/tokens/more)
|
||||
|
||||
- Tutorials
|
||||
|
||||
|
||||
@@ -57,7 +57,18 @@
|
||||
`;
|
||||
})
|
||||
.join('')}
|
||||
</tbody>
|
||||
|
||||
<tr>
|
||||
<td class="nowrap"><code>updateComplete</code></td>
|
||||
<td>
|
||||
A read-only promise that resolves when the component has
|
||||
<a href="/getting-started/usage?id=component-rendering-and-updating">finished updating</a>.
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
`;
|
||||
|
||||
return table.outerHTML;
|
||||
@@ -110,7 +121,7 @@
|
||||
.map(
|
||||
method => `
|
||||
<tr>
|
||||
<td class="nowrap"><code>${escapeHtml(method.name)}</code></td>
|
||||
<td class="nowrap"><code>${escapeHtml(method.name)}()</code></td>
|
||||
<td>${escapeHtml(method.description)}</td>
|
||||
<td>
|
||||
${
|
||||
@@ -283,6 +294,7 @@
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" rel="noopener noreferrer" target="_blank">$1</a>')
|
||||
.replace(/`(.*?)`/g, '<code>$1</code>');
|
||||
}
|
||||
|
||||
@@ -434,7 +446,7 @@
|
||||
To import this component from [the CDN](https://www.jsdelivr.com/package/npm/@shoelace-style/shoelace) using a script tag:
|
||||
|
||||
\`\`\`html
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${metadata.package.version}/${component.path}"></script>
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${metadata.package.version}/dist/${component.path}"></script>
|
||||
\`\`\`
|
||||
</sl-tab-panel>
|
||||
|
||||
@@ -442,14 +454,14 @@
|
||||
To import this component from [the CDN](https://www.jsdelivr.com/package/npm/@shoelace-style/shoelace) using a JavaScript import:
|
||||
|
||||
\`\`\`js
|
||||
import 'https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${metadata.package.version}/${component.path}';
|
||||
import 'https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${metadata.package.version}/dist/${component.path}';
|
||||
\`\`\`
|
||||
</sl-tab-panel>
|
||||
|
||||
<sl-tab-panel name="bundler">\n
|
||||
To import this component using [a bundler](/getting-started/installation#bundling):
|
||||
\`\`\`js
|
||||
import '@shoelace-style/shoelace/${component.path}';
|
||||
import '@shoelace-style/shoelace/dist/${component.path}';
|
||||
\`\`\`
|
||||
</sl-tab-panel>
|
||||
|
||||
@@ -484,12 +496,21 @@
|
||||
`;
|
||||
}
|
||||
|
||||
if (component.slots?.length) {
|
||||
result += `
|
||||
## Slots
|
||||
${createSlotsTable(component.slots)}
|
||||
|
||||
_Learn more about [using slots](/getting-started/usage#slots)._
|
||||
`;
|
||||
}
|
||||
|
||||
if (props?.length) {
|
||||
result += `
|
||||
## Properties
|
||||
## Attributes & Properties
|
||||
${createPropsTable(props)}
|
||||
|
||||
_Learn more about [properties and attributes](/getting-started/usage#properties)._
|
||||
_Learn more about [attributes and properties](/getting-started/usage#properties)._
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -512,15 +533,6 @@
|
||||
`;
|
||||
}
|
||||
|
||||
if (component.slots?.length) {
|
||||
result += `
|
||||
## Slots
|
||||
${createSlotsTable(component.slots)}
|
||||
|
||||
_Learn more about [using slots](/getting-started/usage#slots)._
|
||||
`;
|
||||
}
|
||||
|
||||
if (component.cssProperties?.length) {
|
||||
result += `
|
||||
## CSS Custom Properties
|
||||
@@ -552,7 +564,7 @@
|
||||
result += `
|
||||
## Dependencies
|
||||
|
||||
This component imports the following dependencies.
|
||||
This component automatically imports the following dependencies.
|
||||
|
||||
${createDependenciesList(component.tagName, getAllComponents(metadata))}
|
||||
`;
|
||||
|
||||
@@ -47,10 +47,10 @@
|
||||
</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-label>Toggle <kbd>\\</kbd></sl-menu-label>
|
||||
<sl-menu-item value="light">Light</sl-menu-item>
|
||||
<sl-menu-item value="dark">Dark</sl-menu-item>
|
||||
<sl-menu-item type="checkbox" value="light">Light</sl-menu-item>
|
||||
<sl-menu-item type="checkbox" value="dark">Dark</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item value="auto">Auto</sl-menu-item>
|
||||
<sl-menu-item type="checkbox" value="auto">Auto</sl-menu-item>
|
||||
</sl-menu>
|
||||
`;
|
||||
document.querySelector('.sidebar-toggle').insertAdjacentElement('afterend', dropdown);
|
||||
|
||||
@@ -91,19 +91,19 @@ This example demonstrates all of the baked-in animations and easings. Animations
|
||||
const easings = getEasingNames();
|
||||
|
||||
animations.map(name => {
|
||||
const menuItem = Object.assign(document.createElement('sl-menu-item'), {
|
||||
const option = Object.assign(document.createElement('sl-option'), {
|
||||
textContent: name,
|
||||
value: name
|
||||
});
|
||||
animationName.appendChild(menuItem);
|
||||
animationName.appendChild(option);
|
||||
});
|
||||
|
||||
easings.map(name => {
|
||||
const menuItem = Object.assign(document.createElement('sl-menu-item'), {
|
||||
const option = Object.assign(document.createElement('sl-option'), {
|
||||
textContent: name,
|
||||
value: name
|
||||
});
|
||||
easingName.appendChild(menuItem);
|
||||
easingName.appendChild(option);
|
||||
});
|
||||
|
||||
animationName.addEventListener('sl-change', () => (animation.name = animationName.value));
|
||||
|
||||
@@ -203,9 +203,9 @@ Dropdown menus can be placed in a prefix or suffix slot to provide additional op
|
||||
<sl-icon label="More options" name="three-dots"></sl-icon>
|
||||
</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item checked>Web Design</sl-menu-item>
|
||||
<sl-menu-item>Web Development</sl-menu-item>
|
||||
<sl-menu-item>Marketing</sl-menu-item>
|
||||
<sl-menu-item type="checkbox" checked>Web Design</sl-menu-item>
|
||||
<sl-menu-item type="checkbox">Web Development</sl-menu-item>
|
||||
<sl-menu-item type="checkbox">Marketing</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
</sl-breadcrumb-item>
|
||||
@@ -235,9 +235,11 @@ const App = () => (
|
||||
<SlIcon label="More options" name="three-dots"></SlIcon>
|
||||
</SlButton>
|
||||
<SlMenu>
|
||||
<SlMenuItem checked>Web Design</SlMenuItem>
|
||||
<SlMenuItem>Web Development</SlMenuItem>
|
||||
<SlMenuItem>Marketing</SlMenuItem>
|
||||
<SlMenuItem type="checkbox" checked>
|
||||
Web Design
|
||||
</SlMenuItem>
|
||||
<SlMenuItem type="checkbox">Web Development</SlMenuItem>
|
||||
<SlMenuItem type="checkbox">Marketing</SlMenuItem>
|
||||
</SlMenu>
|
||||
</SlDropdown>
|
||||
</SlBreadcrumbItem>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
[component-header:sl-button-group]
|
||||
|
||||
```html preview
|
||||
<sl-button-group>
|
||||
<sl-button-group label="Alignment">
|
||||
<sl-button>Left</sl-button>
|
||||
<sl-button>Center</sl-button>
|
||||
<sl-button>Right</sl-button>
|
||||
@@ -14,7 +14,7 @@
|
||||
import { SlButton, SlButtonGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlButtonGroup>
|
||||
<SlButtonGroup label="Alignment">
|
||||
<SlButton>Left</SlButton>
|
||||
<SlButton>Center</SlButton>
|
||||
<SlButton>Right</SlButton>
|
||||
@@ -29,7 +29,7 @@ const App = () => (
|
||||
All button sizes are supported, but avoid mixing sizes within the same button group.
|
||||
|
||||
```html preview
|
||||
<sl-button-group>
|
||||
<sl-button-group label="Alignment">
|
||||
<sl-button size="small">Left</sl-button>
|
||||
<sl-button size="small">Center</sl-button>
|
||||
<sl-button size="small">Right</sl-button>
|
||||
@@ -37,7 +37,7 @@ All button sizes are supported, but avoid mixing sizes within the same button gr
|
||||
|
||||
<br /><br />
|
||||
|
||||
<sl-button-group>
|
||||
<sl-button-group label="Alignment">
|
||||
<sl-button size="medium">Left</sl-button>
|
||||
<sl-button size="medium">Center</sl-button>
|
||||
<sl-button size="medium">Right</sl-button>
|
||||
@@ -45,7 +45,7 @@ All button sizes are supported, but avoid mixing sizes within the same button gr
|
||||
|
||||
<br /><br />
|
||||
|
||||
<sl-button-group>
|
||||
<sl-button-group label="Alignment">
|
||||
<sl-button size="large">Left</sl-button>
|
||||
<sl-button size="large">Center</sl-button>
|
||||
<sl-button size="large">Right</sl-button>
|
||||
@@ -57,7 +57,7 @@ import { SlButton, SlButtonGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlButtonGroup>
|
||||
<SlButtonGroup label="Alignment">
|
||||
<SlButton size="small">Left</SlButton>
|
||||
<SlButton size="small">Center</SlButton>
|
||||
<SlButton size="small">Right</SlButton>
|
||||
@@ -66,7 +66,7 @@ const App = () => (
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<SlButtonGroup>
|
||||
<SlButtonGroup label="Alignment">
|
||||
<SlButton size="medium">Left</SlButton>
|
||||
<SlButton size="medium">Center</SlButton>
|
||||
<SlButton size="medium">Right</SlButton>
|
||||
@@ -75,7 +75,7 @@ const App = () => (
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<SlButtonGroup>
|
||||
<SlButtonGroup label="Alignment">
|
||||
<SlButton size="large">Left</SlButton>
|
||||
<SlButton size="large">Center</SlButton>
|
||||
<SlButton size="large">Right</SlButton>
|
||||
@@ -89,7 +89,7 @@ const App = () => (
|
||||
Theme buttons are supported through the button's `variant` attribute.
|
||||
|
||||
```html preview
|
||||
<sl-button-group>
|
||||
<sl-button-group label="Alignment">
|
||||
<sl-button variant="primary">Left</sl-button>
|
||||
<sl-button variant="primary">Center</sl-button>
|
||||
<sl-button variant="primary">Right</sl-button>
|
||||
@@ -97,7 +97,7 @@ Theme buttons are supported through the button's `variant` attribute.
|
||||
|
||||
<br /><br />
|
||||
|
||||
<sl-button-group>
|
||||
<sl-button-group label="Alignment">
|
||||
<sl-button variant="success">Left</sl-button>
|
||||
<sl-button variant="success">Center</sl-button>
|
||||
<sl-button variant="success">Right</sl-button>
|
||||
@@ -105,7 +105,7 @@ Theme buttons are supported through the button's `variant` attribute.
|
||||
|
||||
<br /><br />
|
||||
|
||||
<sl-button-group>
|
||||
<sl-button-group label="Alignment">
|
||||
<sl-button variant="neutral">Left</sl-button>
|
||||
<sl-button variant="neutral">Center</sl-button>
|
||||
<sl-button variant="neutral">Right</sl-button>
|
||||
@@ -113,7 +113,7 @@ Theme buttons are supported through the button's `variant` attribute.
|
||||
|
||||
<br /><br />
|
||||
|
||||
<sl-button-group>
|
||||
<sl-button-group label="Alignment">
|
||||
<sl-button variant="warning">Left</sl-button>
|
||||
<sl-button variant="warning">Center</sl-button>
|
||||
<sl-button variant="warning">Right</sl-button>
|
||||
@@ -121,7 +121,7 @@ Theme buttons are supported through the button's `variant` attribute.
|
||||
|
||||
<br /><br />
|
||||
|
||||
<sl-button-group>
|
||||
<sl-button-group label="Alignment">
|
||||
<sl-button variant="danger">Left</sl-button>
|
||||
<sl-button variant="danger">Center</sl-button>
|
||||
<sl-button variant="danger">Right</sl-button>
|
||||
@@ -133,7 +133,7 @@ import { SlButton, SlButtonGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlButtonGroup>
|
||||
<SlButtonGroup label="Alignment">
|
||||
<SlButton variant="primary">Left</SlButton>
|
||||
<SlButton variant="primary">Center</SlButton>
|
||||
<SlButton variant="primary">Right</SlButton>
|
||||
@@ -142,7 +142,7 @@ const App = () => (
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<SlButtonGroup>
|
||||
<SlButtonGroup label="Alignment">
|
||||
<SlButton variant="success">Left</SlButton>
|
||||
<SlButton variant="success">Center</SlButton>
|
||||
<SlButton variant="success">Right</SlButton>
|
||||
@@ -151,7 +151,7 @@ const App = () => (
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<SlButtonGroup>
|
||||
<SlButtonGroup label="Alignment">
|
||||
<SlButton variant="neutral">Left</SlButton>
|
||||
<SlButton variant="neutral">Center</SlButton>
|
||||
<SlButton variant="neutral">Right</SlButton>
|
||||
@@ -160,7 +160,7 @@ const App = () => (
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<SlButtonGroup>
|
||||
<SlButtonGroup label="Alignment">
|
||||
<SlButton variant="warning">Left</SlButton>
|
||||
<SlButton variant="warning">Center</SlButton>
|
||||
<SlButton variant="warning">Right</SlButton>
|
||||
@@ -169,7 +169,7 @@ const App = () => (
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<SlButtonGroup>
|
||||
<SlButtonGroup label="Alignment">
|
||||
<SlButton variant="danger">Left</SlButton>
|
||||
<SlButton variant="danger">Center</SlButton>
|
||||
<SlButton variant="danger">Right</SlButton>
|
||||
@@ -183,7 +183,7 @@ const App = () => (
|
||||
Pill buttons are supported through the button's `pill` attribute.
|
||||
|
||||
```html preview
|
||||
<sl-button-group>
|
||||
<sl-button-group label="Alignment">
|
||||
<sl-button size="small" pill>Left</sl-button>
|
||||
<sl-button size="small" pill>Center</sl-button>
|
||||
<sl-button size="small" pill>Right</sl-button>
|
||||
@@ -191,7 +191,7 @@ Pill buttons are supported through the button's `pill` attribute.
|
||||
|
||||
<br /><br />
|
||||
|
||||
<sl-button-group>
|
||||
<sl-button-group label="Alignment">
|
||||
<sl-button size="medium" pill>Left</sl-button>
|
||||
<sl-button size="medium" pill>Center</sl-button>
|
||||
<sl-button size="medium" pill>Right</sl-button>
|
||||
@@ -199,7 +199,7 @@ Pill buttons are supported through the button's `pill` attribute.
|
||||
|
||||
<br /><br />
|
||||
|
||||
<sl-button-group>
|
||||
<sl-button-group label="Alignment">
|
||||
<sl-button size="large" pill>Left</sl-button>
|
||||
<sl-button size="large" pill>Center</sl-button>
|
||||
<sl-button size="large" pill>Right</sl-button>
|
||||
@@ -211,7 +211,7 @@ import { SlButton, SlButtonGroup } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlButtonGroup>
|
||||
<SlButtonGroup label="Alignment">
|
||||
<SlButton size="small" pill>
|
||||
Left
|
||||
</SlButton>
|
||||
@@ -226,7 +226,7 @@ const App = () => (
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<SlButtonGroup>
|
||||
<SlButtonGroup label="Alignment">
|
||||
<SlButton size="medium" pill>
|
||||
Left
|
||||
</SlButton>
|
||||
@@ -241,7 +241,7 @@ const App = () => (
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<SlButtonGroup>
|
||||
<SlButtonGroup label="Alignment">
|
||||
<SlButton size="large" pill>
|
||||
Left
|
||||
</SlButton>
|
||||
@@ -261,7 +261,7 @@ const App = () => (
|
||||
Dropdowns can be placed inside button groups as long as the trigger is an `<sl-button>` element.
|
||||
|
||||
```html preview
|
||||
<sl-button-group>
|
||||
<sl-button-group label="Example Button Group">
|
||||
<sl-button>Button</sl-button>
|
||||
<sl-button>Button</sl-button>
|
||||
<sl-dropdown>
|
||||
@@ -279,7 +279,7 @@ Dropdowns can be placed inside button groups as long as the trigger is an `<sl-b
|
||||
import { SlButton, SlButtonGroup, SlDropdown, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlButtonGroup>
|
||||
<SlButtonGroup label="Example Button Group">
|
||||
<SlButton>Button</SlButton>
|
||||
<SlButton>Button</SlButton>
|
||||
<SlDropdown>
|
||||
@@ -301,7 +301,7 @@ const App = () => (
|
||||
Create a split button using a button and a dropdown. Use a [visually hidden](/components/visually-hidden) label to ensure the dropdown is accessible to users with assistive devices.
|
||||
|
||||
```html preview
|
||||
<sl-button-group>
|
||||
<sl-button-group label="Example Button Group">
|
||||
<sl-button variant="primary">Save</sl-button>
|
||||
<sl-dropdown placement="bottom-end">
|
||||
<sl-button slot="trigger" variant="primary" caret>
|
||||
@@ -320,7 +320,7 @@ Create a split button using a button and a dropdown. Use a [visually hidden](/co
|
||||
import { SlButton, SlButtonGroup, SlDropdown, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlButtonGroup>
|
||||
<SlButtonGroup label="Example Button Group">
|
||||
<SlButton variant="primary">Save</SlButton>
|
||||
<SlDropdown placement="bottom-end">
|
||||
<SlButton slot="trigger" variant="primary" caret></SlButton>
|
||||
@@ -339,7 +339,7 @@ const App = () => (
|
||||
Buttons can be wrapped in tooltips to provide more detail when the user interacts with them.
|
||||
|
||||
```html preview
|
||||
<sl-button-group>
|
||||
<sl-button-group label="Alignment">
|
||||
<sl-tooltip content="I'm on the left">
|
||||
<sl-button>Left</sl-button>
|
||||
</sl-tooltip>
|
||||
@@ -359,7 +359,7 @@ import { SlButton, SlButtonGroup, SlTooltip } from '@shoelace-style/shoelace/dis
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlButtonGroup>
|
||||
<SlButtonGroup label="Alignment">
|
||||
<SlTooltip content="I'm on the left">
|
||||
<SlButton>Left</SlButton>
|
||||
</SlTooltip>
|
||||
|
||||
@@ -134,7 +134,7 @@ const App = () => (
|
||||
|
||||
### Circle Buttons
|
||||
|
||||
Use the `circle` attribute to create circular icon buttons.
|
||||
Use the `circle` attribute to create circular icon buttons. When this attribute is set, the button expects a single `<sl-icon>` in the default slot.
|
||||
|
||||
```html preview
|
||||
<sl-button variant="default" size="small" circle>
|
||||
|
||||
@@ -58,6 +58,32 @@ import { SlCheckbox } from '@shoelace-style/shoelace/dist/react';
|
||||
const App = () => <SlCheckbox disabled>Disabled</SlCheckbox>;
|
||||
```
|
||||
|
||||
## Sizes
|
||||
|
||||
Use the `size` attribute to change a checkbox's size.
|
||||
|
||||
```html preview
|
||||
<sl-checkbox size="small">Small</sl-checkbox>
|
||||
<br />
|
||||
<sl-checkbox size="medium">Medium</sl-checkbox>
|
||||
<br />
|
||||
<sl-checkbox size="large">Large</sl-checkbox>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlCheckbox } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlCheckbox size="small">Small</SlCheckbox>
|
||||
<br />
|
||||
<SlCheckbox size="medium">Medium</SlCheckbox>
|
||||
<br />
|
||||
<SlCheckbox size="large">Large</SlCheckbox>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Custom Validity
|
||||
|
||||
Use the `setCustomValidity()` method to set a custom validation message. This will prevent the form from submitting and make the browser display the error message you provide. To clear the error, call this function with an empty string.
|
||||
|
||||
@@ -32,7 +32,7 @@ const App = () => <SlColorPicker value="#4a90e2" label="Select a color" />;
|
||||
|
||||
### Opacity
|
||||
|
||||
Use the `opacity` attribute to enable the opacity slider. When this is enabled, the value will be displayed as HEXA, RGBA, or HSLA based on `format`.
|
||||
Use the `opacity` attribute to enable the opacity slider. When this is enabled, the value will be displayed as HEXA, RGBA, HSLA, or HSVA based on `format`.
|
||||
|
||||
```html preview
|
||||
<sl-color-picker value="#f5a623ff" opacity label="Select a color"></sl-color-picker>
|
||||
@@ -46,7 +46,7 @@ const App = () => <SlColorPicker opacity label="Select a color" />;
|
||||
|
||||
### Formats
|
||||
|
||||
Set the color picker's format with the `format` attribute. Valid options include `hex`, `rgb`, and `hsl`. Note that the color picker's input will accept any parsable format (including CSS color names) regardless of this option.
|
||||
Set the color picker's format with the `format` attribute. Valid options include `hex`, `rgb`, `hsl`, and `hsv`. Note that the color picker's input will accept any parsable format (including CSS color names) regardless of this option.
|
||||
|
||||
To prevent users from toggling the format themselves, add the `no-format-toggle` attribute.
|
||||
|
||||
@@ -54,6 +54,7 @@ To prevent users from toggling the format themselves, add the `no-format-toggle`
|
||||
<sl-color-picker format="hex" value="#4a90e2" label="Select a color"></sl-color-picker>
|
||||
<sl-color-picker format="rgb" value="rgb(80, 227, 194)" label="Select a color"></sl-color-picker>
|
||||
<sl-color-picker format="hsl" value="hsl(290, 87%, 47%)" label="Select a color"></sl-color-picker>
|
||||
<sl-color-picker format="hsv" value="hsv(55, 89%, 97%)" label="Select a color"></sl-color-picker>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
@@ -64,10 +65,39 @@ const App = () => (
|
||||
<SlColorPicker format="hex" value="#4a90e2" />
|
||||
<SlColorPicker format="rgb" value="rgb(80, 227, 194)" />
|
||||
<SlColorPicker format="hsl" value="hsl(290, 87%, 47%)" />
|
||||
<SlColorPicker format="hsv" value="hsv(55, 89%, 97%)" />
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Swatches
|
||||
|
||||
Use the `swatches` attribute to add convenient presets to the color picker. Any format the color picker can parse is acceptable (including CSS color names), but each value must be separated by a semicolon (`;`). Alternatively, you can pass an array of color values to this property using JavaScript.
|
||||
|
||||
```html preview
|
||||
<sl-color-picker
|
||||
label="Select a color"
|
||||
swatches="
|
||||
#d0021b; #f5a623; #f8e71c; #8b572a; #7ed321; #417505; #bd10e0; #9013fe;
|
||||
#4a90e2; #50e3c2; #b8e986; #000; #444; #888; #ccc; #fff;
|
||||
"
|
||||
></sl-color-picker>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlColorPicker } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlColorPicker
|
||||
label="Select a color"
|
||||
swatches="
|
||||
#d0021b; #f5a623; #f8e71c; #8b572a; #7ed321; #417505; #bd10e0; #9013fe;
|
||||
#4a90e2; #50e3c2; #b8e986; #000; #444; #888; #ccc; #fff;
|
||||
"
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
### Sizes
|
||||
|
||||
Use the `size` attribute to change the color picker's trigger size.
|
||||
|
||||
@@ -46,6 +46,52 @@ const App = () => (
|
||||
);
|
||||
```
|
||||
|
||||
### Customizing the Summary Icon
|
||||
|
||||
Use the `expand-icon` and `collapse-icon` slots to change the expand and collapse icons, respectively. To disable the animation, override the `rotate` property on the `summary-icon` part as shown below.
|
||||
|
||||
```html preview
|
||||
<sl-details summary="Toggle Me" class="custom-icons">
|
||||
<sl-icon name="plus-square" slot="expand-icon"></sl-icon>
|
||||
<sl-icon name="dash-square" slot="collapse-icon"></sl-icon>
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna
|
||||
aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</sl-details>
|
||||
|
||||
<style>
|
||||
sl-details.custom-icons::part(summary-icon) {
|
||||
/* Disable the expand/collapse animation */
|
||||
rotate: none;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlDetails, SlIcon } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const css = `
|
||||
sl-details.custom-icon::part(summary-icon) {
|
||||
/* Disable the expand/collapse animation */
|
||||
rotate: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlDetails summary="Toggle Me" class="custom-icon">
|
||||
<SlIcon name="plus-square" slot="expand-icon" />
|
||||
<SlIcon name="dash-square" slot="collapse-icon" />
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore
|
||||
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
|
||||
consequat.
|
||||
</SlDetails>
|
||||
|
||||
<style>{css}</style>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Grouping Details
|
||||
|
||||
Details are designed to function independently, but you can simulate a group or "accordion" where only one is shown at a time by listening for the `sl-show` event.
|
||||
|
||||
@@ -151,6 +151,59 @@ const App = () => {
|
||||
};
|
||||
```
|
||||
|
||||
### Header Actions
|
||||
|
||||
The header shows a functional close button by default. You can use the `header-actions` slot to add additional [icon buttons](/components/icon-button) if needed.
|
||||
|
||||
```html preview
|
||||
<sl-dialog label="Dialog" class="dialog-header-actions">
|
||||
<sl-icon-button class="new-window" slot="header-actions" name="box-arrow-up-right"></sl-icon-button>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
<sl-button slot="footer" variant="primary">Close</sl-button>
|
||||
</sl-dialog>
|
||||
|
||||
<sl-button>Open Dialog</sl-button>
|
||||
|
||||
<script>
|
||||
const dialog = document.querySelector('.dialog-header-actions');
|
||||
const openButton = dialog.nextElementSibling;
|
||||
const closeButton = dialog.querySelector('sl-button[slot="footer"]');
|
||||
const newWindowButton = dialog.querySelector('.new-window');
|
||||
|
||||
openButton.addEventListener('click', () => dialog.show());
|
||||
closeButton.addEventListener('click', () => dialog.hide());
|
||||
newWindowButton.addEventListener('click', () => window.open(location.href));
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { useState } from 'react';
|
||||
import { SlButton, SlDialog, SlIconButton } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SlDialog label="Dialog" open={open} onSlAfterHide={() => setOpen(false)}>
|
||||
<SlIconButton
|
||||
class="new-window"
|
||||
slot="header-actions"
|
||||
name="box-arrow-up-right"
|
||||
onClick={() => window.open(location.href)}
|
||||
/>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
<SlButton slot="footer" variant="primary" onClick={() => setOpen(false)}>
|
||||
Close
|
||||
</SlButton>
|
||||
</SlDialog>
|
||||
|
||||
<SlButton onClick={() => setOpen(true)}>Open Dialog</SlButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Preventing the Dialog from Closing
|
||||
|
||||
By default, dialogs will close when the user clicks the close button, clicks the overlay, or presses the <kbd>Escape</kbd> key. In most cases, the default behavior is the best behavior in terms of UX. However, there are situations where this may be undesirable, such as when data loss will occur.
|
||||
|
||||
@@ -180,7 +180,9 @@ const App = () => {
|
||||
|
||||
### Contained to an Element
|
||||
|
||||
By default, the drawer slides out of its [containing block](https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#Identifying_the_containing_block), which is usually the viewport. To make the drawer slide out of its parent element, add the `contained` attribute and `position: relative` to the parent.
|
||||
By default, drawers slide out of their [containing block](https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#Identifying_the_containing_block), which is usually the viewport. To make a drawer slide out of a parent element, add the `contained` attribute to the drawer and apply `position: relative` to its parent.
|
||||
|
||||
Unlike normal drawers, contained drawers are not modal. This means they do not show an overlay, they do not trap focus, and they are not dismissible with <kbd>Escape</kbd>. This is intentional to allow users to interact with elements outside of the drawer.
|
||||
|
||||
```html preview
|
||||
<div
|
||||
@@ -194,14 +196,14 @@ By default, the drawer slides out of its [containing block](https://developer.mo
|
||||
</sl-drawer>
|
||||
</div>
|
||||
|
||||
<sl-button>Open Drawer</sl-button>
|
||||
<sl-button>Toggle Drawer</sl-button>
|
||||
|
||||
<script>
|
||||
const drawer = document.querySelector('.drawer-contained');
|
||||
const openButton = drawer.parentElement.nextElementSibling;
|
||||
const closeButton = drawer.querySelector('sl-button[variant="primary"]');
|
||||
|
||||
openButton.addEventListener('click', () => drawer.show());
|
||||
openButton.addEventListener('click', () => (drawer.open = !drawer.open));
|
||||
closeButton.addEventListener('click', () => drawer.hide());
|
||||
</script>
|
||||
```
|
||||
@@ -226,7 +228,14 @@ const App = () => {
|
||||
>
|
||||
The drawer will be contained to this box. This content won't shift or be affected in any way when the drawer
|
||||
opens.
|
||||
<SlDrawer label="Drawer" contained open={open} onSlAfterHide={() => setOpen(false)} style={{ '--size': '50%' }}>
|
||||
<SlDrawer
|
||||
label="Drawer"
|
||||
contained
|
||||
no-modal
|
||||
open={open}
|
||||
onSlAfterHide={() => setOpen(false)}
|
||||
style={{ '--size': '50%' }}
|
||||
>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
<SlButton slot="footer" variant="primary" onClick={() => setOpen(false)}>
|
||||
Close
|
||||
@@ -338,6 +347,54 @@ const App = () => {
|
||||
};
|
||||
```
|
||||
|
||||
### Header Actions
|
||||
|
||||
The header shows a functional close button by default. You can use the `header-actions` slot to add additional [icon buttons](/components/icon-button) if needed.
|
||||
|
||||
```html preview
|
||||
<sl-drawer label="Drawer" class="drawer-header-actions">
|
||||
<sl-icon-button class="new-window" slot="header-actions" name="box-arrow-up-right"></sl-icon-button>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
<sl-button slot="footer" variant="primary">Close</sl-button>
|
||||
</sl-drawer>
|
||||
|
||||
<sl-button>Open Drawer</sl-button>
|
||||
|
||||
<script>
|
||||
const drawer = document.querySelector('.drawer-header-actions');
|
||||
const openButton = drawer.nextElementSibling;
|
||||
const closeButton = drawer.querySelector('sl-button[variant="primary"]');
|
||||
const newWindowButton = drawer.querySelector('.new-window');
|
||||
|
||||
openButton.addEventListener('click', () => drawer.show());
|
||||
closeButton.addEventListener('click', () => drawer.hide());
|
||||
newWindowButton.addEventListener('click', () => window.open(location.href));
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { useState } from 'react';
|
||||
import { SlButton, SlDrawer, SlIconButton } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SlDrawer label="Drawer" open={open} onSlAfterHide={() => setOpen(false)}>
|
||||
<SlIconButton slot="header-actions" name="box-arrow-up-right" onClick={() => window.open(location.href)} />
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||
<SlButton slot="footer" variant="primary" onClick={() => setOpen(false)}>
|
||||
Close
|
||||
</SlButton>
|
||||
</SlDrawer>
|
||||
|
||||
<SlButton onClick={() => setOpen(true)}>Open Drawer</SlButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Preventing the Drawer from Closing
|
||||
|
||||
By default, drawers will close when the user clicks the close button, clicks the overlay, or presses the <kbd>Escape</kbd> key. In most cases, the default behavior is the best behavior in terms of UX. However, there are situations where this may be undesirable, such as when data loss will occur.
|
||||
|
||||
@@ -14,7 +14,7 @@ Dropdowns are designed to work well with [menus](/components/menu) to provide a
|
||||
<sl-menu-item>Dropdown Item 2</sl-menu-item>
|
||||
<sl-menu-item>Dropdown Item 3</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item checked>Checked</sl-menu-item>
|
||||
<sl-menu-item type="checkbox" checked>Checkbox</sl-menu-item>
|
||||
<sl-menu-item disabled>Disabled</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item>
|
||||
@@ -42,7 +42,9 @@ const App = () => (
|
||||
<SlMenuItem>Dropdown Item 2</SlMenuItem>
|
||||
<SlMenuItem>Dropdown Item 3</SlMenuItem>
|
||||
<SlDivider />
|
||||
<SlMenuItem checked>Checked</SlMenuItem>
|
||||
<SlMenuItem type="checkbox" checked>
|
||||
Checkbox
|
||||
</SlMenuItem>
|
||||
<SlMenuItem disabled>Disabled</SlMenuItem>
|
||||
<SlDivider />
|
||||
<SlMenuItem>
|
||||
|
||||
@@ -20,9 +20,9 @@ All available icons in the `default` icon library are shown below. Click or tap
|
||||
<sl-icon slot="prefix" name="search"></sl-icon>
|
||||
</sl-input>
|
||||
<sl-select value="outline">
|
||||
<sl-menu-item value="outline">Outlined</sl-menu-item>
|
||||
<sl-menu-item value="fill">Filled</sl-menu-item>
|
||||
<sl-menu-item value="all">All icons</sl-menu-item>
|
||||
<sl-option value="outline">Outlined</sl-option>
|
||||
<sl-option value="fill">Filled</sl-option>
|
||||
<sl-option value="all">All icons</sl-option>
|
||||
</sl-select>
|
||||
</div>
|
||||
<div class="icon-list"></div>
|
||||
|
||||
@@ -77,25 +77,13 @@ const App = () => <SlInput placeholder="Clearable" clearable />;
|
||||
Add the `password-toggle` attribute to add a toggle button that will show the password when activated.
|
||||
|
||||
```html preview
|
||||
<sl-input type="password" placeholder="Password Toggle" size="small" password-toggle></sl-input>
|
||||
<br />
|
||||
<sl-input type="password" placeholder="Password Toggle" size="medium" password-toggle></sl-input>
|
||||
<br />
|
||||
<sl-input type="password" placeholder="Password Toggle" size="large" password-toggle></sl-input>
|
||||
<sl-input type="password" placeholder="Password Toggle" password-toggle></sl-input>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlInput type="password" placeholder="Password Toggle" size="small" password-toggle />
|
||||
<br />
|
||||
<SlInput type="password" placeholder="Password Toggle" size="medium" password-toggle />
|
||||
<br />
|
||||
<SlInput type="password" placeholder="Password Toggle" size="large" password-toggle />
|
||||
</>
|
||||
);
|
||||
const App = () => <SlInput type="password" placeholder="Password Toggle" size="medium" password-toggle />;
|
||||
```
|
||||
|
||||
### Filled Inputs
|
||||
@@ -112,6 +100,46 @@ import { SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
const App = () => <SlInput placeholder="Type something" filled />;
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` attribute to disable an input.
|
||||
|
||||
```html preview
|
||||
<sl-input placeholder="Disabled" disabled></sl-input>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => <SlInput placeholder="Disabled" disabled />;
|
||||
```
|
||||
|
||||
### Sizes
|
||||
|
||||
Use the `size` attribute to change an input's size.
|
||||
|
||||
```html preview
|
||||
<sl-input placeholder="Small" size="small"></sl-input>
|
||||
<br />
|
||||
<sl-input placeholder="Medium" size="medium"></sl-input>
|
||||
<br />
|
||||
<sl-input placeholder="Large" size="large"></sl-input>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlInput placeholder="Small" size="small" />
|
||||
<br />
|
||||
<SlInput placeholder="Medium" size="medium" />
|
||||
<br />
|
||||
<SlInput placeholder="Large" size="large" />
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Pill
|
||||
|
||||
Use the `pill` attribute to give inputs rounded edges.
|
||||
@@ -164,58 +192,6 @@ const App = () => (
|
||||
);
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` attribute to disable an input.
|
||||
|
||||
```html preview
|
||||
<sl-input placeholder="Disabled" size="small" disabled></sl-input>
|
||||
<br />
|
||||
<sl-input placeholder="Disabled" size="medium" disabled></sl-input>
|
||||
<br />
|
||||
<sl-input placeholder="Disabled" size="large" disabled></sl-input>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlInput placeholder="Disabled" size="small" disabled />
|
||||
<br />
|
||||
<SlInput placeholder="Disabled" size="medium" disabled />
|
||||
<br />
|
||||
<SlInput placeholder="Disabled" size="large" disabled />
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Sizes
|
||||
|
||||
Use the `size` attribute to change an input's size.
|
||||
|
||||
```html preview
|
||||
<sl-input placeholder="Small" size="small"></sl-input>
|
||||
<br />
|
||||
<sl-input placeholder="Medium" size="medium"></sl-input>
|
||||
<br />
|
||||
<sl-input placeholder="Large" size="large"></sl-input>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlInput } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlInput placeholder="Small" size="small" />
|
||||
<br />
|
||||
<SlInput placeholder="Medium" size="medium" />
|
||||
<br />
|
||||
<SlInput placeholder="Large" size="large" />
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Prefix & Suffix Icons
|
||||
|
||||
Use the `prefix` and `suffix` slots to add icons.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<sl-menu-item>Option 2</sl-menu-item>
|
||||
<sl-menu-item>Option 3</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item checked>Checked</sl-menu-item>
|
||||
<sl-menu-item type="checkbox" checked>Checkbox</sl-menu-item>
|
||||
<sl-menu-item disabled>Disabled</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item>
|
||||
@@ -31,7 +31,9 @@ const App = () => (
|
||||
<SlMenuItem>Option 2</SlMenuItem>
|
||||
<SlMenuItem>Option 3</SlMenuItem>
|
||||
<SlDivider />
|
||||
<SlMenuItem checked>Checked</SlMenuItem>
|
||||
<SlMenuItem type="checkbox" checked>
|
||||
Checkbox
|
||||
</SlMenuItem>
|
||||
<SlMenuItem disabled>Disabled</SlMenuItem>
|
||||
<SlDivider />
|
||||
<SlMenuItem>
|
||||
@@ -48,30 +50,6 @@ const App = () => (
|
||||
|
||||
## Examples
|
||||
|
||||
### Checked
|
||||
|
||||
Use the `checked` attribute to draw menu items in a checked state.
|
||||
|
||||
```html preview
|
||||
<sl-menu style="max-width: 200px;">
|
||||
<sl-menu-item>Option 1</sl-menu-item>
|
||||
<sl-menu-item checked>Option 2</sl-menu-item>
|
||||
<sl-menu-item>Option 3</sl-menu-item>
|
||||
</sl-menu>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlMenu style={{ maxWidth: '200px' }}>
|
||||
<SlMenuItem>Option 1</SlMenuItem>
|
||||
<SlMenuItem checked>Option 2</SlMenuItem>
|
||||
<SlMenuItem>Option 3</SlMenuItem>
|
||||
</SlMenu>
|
||||
);
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
Add the `disabled` attribute to disable the menu item so it cannot be selected.
|
||||
@@ -150,6 +128,34 @@ const App = () => (
|
||||
);
|
||||
```
|
||||
|
||||
### Checkbox Menu Items
|
||||
|
||||
Set the `type` attribute to `checkbox` to create a menu item that will toggle on and off when selected. You can use the `checked` attribute to set the initial state.
|
||||
|
||||
Checkbox menu items are visually indistinguishable from regular menu items. Their ability to be toggled is primarily inferred from context, much like you'd find in the menu of a native app.
|
||||
|
||||
```html preview
|
||||
<sl-menu style="max-width: 200px;">
|
||||
<sl-menu-item type="checkbox">Autosave</sl-menu-item>
|
||||
<sl-menu-item type="checkbox" checked>Check Spelling</sl-menu-item>
|
||||
<sl-menu-item type="checkbox">Word Wrap</sl-menu-item>
|
||||
</sl-menu>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlMenu style={{ maxWidth: '200px' }}>
|
||||
<SlMenuItem type="checkbox">Autosave</SlMenuItem>
|
||||
<SlMenuItem type="checkbox" checked>
|
||||
Check Spelling
|
||||
</SlMenuItem>
|
||||
<SlMenuItem type="checkbox">Word Wrap</SlMenuItem>
|
||||
</SlMenu>
|
||||
);
|
||||
```
|
||||
|
||||
### Value & Selection
|
||||
|
||||
The `value` attribute can be used to assign a hidden value, such as a unique identifier, to a menu item. When an item is selected, the `sl-select` event will be emitted and a reference to the item will be available at `event.detail.item`. You can use this reference to access the selected item's value, its checked state, and more.
|
||||
@@ -159,6 +165,10 @@ The `value` attribute can be used to assign a hidden value, such as a unique ide
|
||||
<sl-menu-item value="opt-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="opt-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="opt-3">Option 3</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item type="checkbox" value="opt-4">Checkbox 4</sl-menu-item>
|
||||
<sl-menu-item type="checkbox" value="opt-5">Checkbox 5</sl-menu-item>
|
||||
<sl-menu-item type="checkbox" value="opt-6">Checkbox 6</sl-menu-item>
|
||||
</sl-menu>
|
||||
|
||||
<script>
|
||||
@@ -167,11 +177,12 @@ The `value` attribute can be used to assign a hidden value, such as a unique ide
|
||||
menu.addEventListener('sl-select', event => {
|
||||
const item = event.detail.item;
|
||||
|
||||
// Toggle checked state
|
||||
item.checked = !item.checked;
|
||||
|
||||
// Log value
|
||||
console.log(`Selected value: ${item.value}`);
|
||||
if (item.type === 'checkbox') {
|
||||
console.log(`Selected value: ${item.value} (${item.checked ? 'checked' : 'unchecked'})`);
|
||||
} else {
|
||||
console.log(`Selected value: ${item.value}`);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
79
docs/components/option.md
Normal file
79
docs/components/option.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Option
|
||||
|
||||
[component-header:sl-option]
|
||||
|
||||
```html preview
|
||||
<sl-select label="Select one">
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` attribute to disable an option and prevent it from being selected.
|
||||
|
||||
```html preview
|
||||
<sl-select label="Select one">
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2" disabled>Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2" disabled>
|
||||
Option 2
|
||||
</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
### Prefix & Suffix
|
||||
|
||||
Add icons to the start and end of menu items using the `prefix` and `suffix` slots.
|
||||
|
||||
```html preview
|
||||
<sl-select label="Select one">
|
||||
<sl-option value="option-1">
|
||||
<sl-icon slot="prefix" name="envelope"></sl-icon>
|
||||
Email
|
||||
<sl-icon slot="suffix" name="patch-check"></sl-icon>
|
||||
</sl-option>
|
||||
|
||||
<sl-option value="option-2">
|
||||
<sl-icon slot="prefix" name="telephone"></sl-icon>
|
||||
Phone
|
||||
<sl-icon slot="suffix" name="patch-check"></sl-icon>
|
||||
</sl-option>
|
||||
|
||||
<sl-option value="option-3">
|
||||
<sl-icon slot="prefix" name="chat-dots"></sl-icon>
|
||||
Chat
|
||||
<sl-icon slot="suffix" name="patch-check"></sl-icon>
|
||||
</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
[component-metadata:sl-option]
|
||||
@@ -6,6 +6,8 @@ This component's name is inspired by [`<popup>`](https://github.com/MicrosoftEdg
|
||||
|
||||
Popup doesn't provide any styles — just positioning! The popup's preferred placement, distance, and skidding (offset) can be configured using attributes. An arrow that points to the anchor can be shown and customized to your liking. Additional positioning options are available and described in more detail below.
|
||||
|
||||
!> Popup is a low-level utility built specifically for positioning elements. Do not mistake it for a [tooltip](/components/tooltip) or similar because _it does not facilitate an accessible experience!_ Almost every correct usage of `<sl-popup>` will involve building other components. It should rarely, if ever, occur directly in your HTML.
|
||||
|
||||
```html preview
|
||||
<div class="popup-overview">
|
||||
<sl-popup placement="top" active>
|
||||
@@ -15,18 +17,18 @@ Popup doesn't provide any styles — just positioning! The popup's preferred pla
|
||||
|
||||
<div class="popup-overview-options">
|
||||
<sl-select label="Placement" name="placement" value="top" class="popup-overview-select">
|
||||
<sl-menu-item value="top">top</sl-menu-item>
|
||||
<sl-menu-item value="top-start">top-start</sl-menu-item>
|
||||
<sl-menu-item value="top-end">top-end</sl-menu-item>
|
||||
<sl-menu-item value="bottom">bottom</sl-menu-item>
|
||||
<sl-menu-item value="bottom-start">bottom-start</sl-menu-item>
|
||||
<sl-menu-item value="bottom-end">bottom-end</sl-menu-item>
|
||||
<sl-menu-item value="right">right</sl-menu-item>
|
||||
<sl-menu-item value="right-start">right-start</sl-menu-item>
|
||||
<sl-menu-item value="right-end">right-end</sl-menu-item>
|
||||
<sl-menu-item value="left">left</sl-menu-item>
|
||||
<sl-menu-item value="left-start">left-start</sl-menu-item>
|
||||
<sl-menu-item value="left-end">left-end</sl-menu-item>
|
||||
<sl-option value="top">top</sl-option>
|
||||
<sl-option value="top-start">top-start</sl-option>
|
||||
<sl-option value="top-end">top-end</sl-option>
|
||||
<sl-option value="bottom">bottom</sl-option>
|
||||
<sl-option value="bottom-start">bottom-start</sl-option>
|
||||
<sl-option value="bottom-end">bottom-end</sl-option>
|
||||
<sl-option value="right">right</sl-option>
|
||||
<sl-option value="right-start">right-start</sl-option>
|
||||
<sl-option value="right-end">right-end</sl-option>
|
||||
<sl-option value="left">left</sl-option>
|
||||
<sl-option value="left-start">left-start</sl-option>
|
||||
<sl-option value="left-end">left-end</sl-option>
|
||||
</sl-select>
|
||||
<sl-input type="number" name="distance" label="distance" value="0"></sl-input>
|
||||
<sl-input type="number" name="skidding" label="Skidding" value="0"></sl-input>
|
||||
@@ -380,18 +382,18 @@ Since placement is preferred when using `flip`, you can observe the popup's curr
|
||||
</sl-popup>
|
||||
|
||||
<sl-select label="Placement" value="top">
|
||||
<sl-menu-item value="top">top</sl-menu-item>
|
||||
<sl-menu-item value="top-start">top-start</sl-menu-item>
|
||||
<sl-menu-item value="top-end">top-end</sl-menu-item>
|
||||
<sl-menu-item value="bottom">bottom</sl-menu-item>
|
||||
<sl-menu-item value="bottom-start">bottom-start</sl-menu-item>
|
||||
<sl-menu-item value="bottom-end">bottom-end</sl-menu-item>
|
||||
<sl-menu-item value="right">right</sl-menu-item>
|
||||
<sl-menu-item value="right-start">right-start</sl-menu-item>
|
||||
<sl-menu-item value="right-end">right-end</sl-menu-item>
|
||||
<sl-menu-item value="left">left</sl-menu-item>
|
||||
<sl-menu-item value="left-start">left-start</sl-menu-item>
|
||||
<sl-menu-item value="left-end">left-end</sl-menu-item>
|
||||
<sl-option value="top">top</sl-option>
|
||||
<sl-option value="top-start">top-start</sl-option>
|
||||
<sl-option value="top-end">top-end</sl-option>
|
||||
<sl-option value="bottom">bottom</sl-option>
|
||||
<sl-option value="bottom-start">bottom-start</sl-option>
|
||||
<sl-option value="bottom-end">bottom-end</sl-option>
|
||||
<sl-option value="right">right</sl-option>
|
||||
<sl-option value="right-start">right-start</sl-option>
|
||||
<sl-option value="right-end">right-end</sl-option>
|
||||
<sl-option value="left">left</sl-option>
|
||||
<sl-option value="left-start">left-start</sl-option>
|
||||
<sl-option value="left-end">left-end</sl-option>
|
||||
</sl-select>
|
||||
</div>
|
||||
|
||||
@@ -523,7 +525,7 @@ Use the `distance` attribute to change the distance between the popup and its an
|
||||
const popup = container.querySelector('sl-popup');
|
||||
const distance = container.querySelector('sl-range');
|
||||
|
||||
distance.addEventListener('sl-change', () => (popup.distance = distance.value));
|
||||
distance.addEventListener('sl-input', () => (popup.distance = distance.value));
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -619,7 +621,7 @@ The `skidding` attribute is similar to `distance`, but instead allows you to off
|
||||
const popup = container.querySelector('sl-popup');
|
||||
const skidding = container.querySelector('sl-range');
|
||||
|
||||
skidding.addEventListener('sl-change', () => (popup.skidding = skidding.value));
|
||||
skidding.addEventListener('sl-input', () => (popup.skidding = skidding.value));
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -690,25 +692,25 @@ By default, the arrow will be aligned as close to the center of the _anchor_ as
|
||||
|
||||
<div class="popup-arrow-options">
|
||||
<sl-select label="Placement" name="placement" value="top" class="popup-overview-select">
|
||||
<sl-menu-item value="top">top</sl-menu-item>
|
||||
<sl-menu-item value="top-start">top-start</sl-menu-item>
|
||||
<sl-menu-item value="top-end">top-end</sl-menu-item>
|
||||
<sl-menu-item value="bottom">bottom</sl-menu-item>
|
||||
<sl-menu-item value="bottom-start">bottom-start</sl-menu-item>
|
||||
<sl-menu-item value="bottom-end">bottom-end</sl-menu-item>
|
||||
<sl-menu-item value="right">right</sl-menu-item>
|
||||
<sl-menu-item value="right-start">right-start</sl-menu-item>
|
||||
<sl-menu-item value="right-end">right-end</sl-menu-item>
|
||||
<sl-menu-item value="left">left</sl-menu-item>
|
||||
<sl-menu-item value="left-start">left-start</sl-menu-item>
|
||||
<sl-menu-item value="left-end">left-end</sl-menu-item>
|
||||
<sl-option value="top">top</sl-option>
|
||||
<sl-option value="top-start">top-start</sl-option>
|
||||
<sl-option value="top-end">top-end</sl-option>
|
||||
<sl-option value="bottom">bottom</sl-option>
|
||||
<sl-option value="bottom-start">bottom-start</sl-option>
|
||||
<sl-option value="bottom-end">bottom-end</sl-option>
|
||||
<sl-option value="right">right</sl-option>
|
||||
<sl-option value="right-start">right-start</sl-option>
|
||||
<sl-option value="right-end">right-end</sl-option>
|
||||
<sl-option value="left">left</sl-option>
|
||||
<sl-option value="left-start">left-start</sl-option>
|
||||
<sl-option value="left-end">left-end</sl-option>
|
||||
</sl-select>
|
||||
|
||||
<sl-select label="Arrow Placement" name="arrow-placement" value="anchor">
|
||||
<sl-menu-item value="anchor">anchor</sl-menu-item>
|
||||
<sl-menu-item value="start">start</sl-menu-item>
|
||||
<sl-menu-item value="end">end</sl-menu-item>
|
||||
<sl-menu-item value="center">center</sl-menu-item>
|
||||
<sl-option value="anchor">anchor</sl-option>
|
||||
<sl-option value="start">start</sl-option>
|
||||
<sl-option value="end">end</sl-option>
|
||||
<sl-option value="center">center</sl-option>
|
||||
</sl-select>
|
||||
</div>
|
||||
|
||||
@@ -879,10 +881,10 @@ Use the `sync` attribute to make the popup the same width or height as the ancho
|
||||
</sl-popup>
|
||||
|
||||
<sl-select value="width" label="Sync">
|
||||
<sl-menu-item value="width">Width</sl-menu-item>
|
||||
<sl-menu-item value="height">Height</sl-menu-item>
|
||||
<sl-menu-item value="both">Both</sl-menu-item>
|
||||
<sl-menu-item value="">None</sl-menu-item>
|
||||
<sl-option value="width">Width</sl-option>
|
||||
<sl-option value="height">Height</sl-option>
|
||||
<sl-option value="both">Both</sl-option>
|
||||
<sl-option value="">None</sl-option>
|
||||
</sl-select>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
[component-header:sl-qr-code]
|
||||
|
||||
Generates a [QR code](https://www.qrcode.com/) and renders it using the [Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API).
|
||||
|
||||
QR codes are useful for providing small pieces of information to users who can quickly scan them with a smartphone. Most smartphones have built-in QR code scanners, so simply pointing the camera at a QR code will decode it and allow the user to visit a website, dial a phone number, read a message, etc.
|
||||
|
||||
```html preview
|
||||
|
||||
@@ -78,4 +78,28 @@ const App = () => (
|
||||
);
|
||||
```
|
||||
|
||||
## Sizes
|
||||
|
||||
Use the `size` attribute to change a radio's size.
|
||||
|
||||
```html preview
|
||||
<sl-radio size="small">Small</sl-radio>
|
||||
<sl-radio size="medium">Medium</sl-radio>
|
||||
<sl-radio size="large">Large</sl-radio>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlRadio } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlRadio size="small">Small</SlRadio>
|
||||
<br />
|
||||
<SlRadio size="medium">Medium</SlRadio>
|
||||
<br />
|
||||
<SlRadio size="large">Large</SlRadio>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
[component-metadata:sl-radio]
|
||||
|
||||
@@ -56,7 +56,7 @@ import { SlRating } from '@shoelace-style/shoelace/dist/react';
|
||||
const App = () => <SlRating label="Rating" precision={0.5} value={2.5} />;
|
||||
```
|
||||
|
||||
## Symbol Sizes
|
||||
### Symbol Sizes
|
||||
|
||||
Set the `--symbol-size` custom property to adjust the size.
|
||||
|
||||
@@ -98,6 +98,99 @@ import { SlRating } from '@shoelace-style/shoelace/dist/react';
|
||||
const App = () => <SlRating label="Rating" disabled value={3} />;
|
||||
```
|
||||
|
||||
### Detecting Hover
|
||||
|
||||
Use the `sl-hover` event to detect when the user hovers over (or touch and drag) the rating. This lets you hook into values as the user interacts with the rating, but before they select a value.
|
||||
|
||||
The event has a payload with `phase` and `value` properties. The `phase` property tells when hovering starts, moves to a new value, and ends. The `value` property tells what the rating's value would be if the user were to commit to the hovered value.
|
||||
|
||||
```html preview
|
||||
<div class="detect-hover">
|
||||
<sl-rating label="Rating"></sl-rating>
|
||||
<span></span>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const rating = document.querySelector('.detect-hover > sl-rating');
|
||||
const span = rating.nextElementSibling;
|
||||
const terms = ['No rating', 'Terrible', 'Bad', 'OK', 'Good', 'Excellent'];
|
||||
|
||||
rating.addEventListener('sl-hover', event => {
|
||||
span.textContent = terms[event.detail.value];
|
||||
|
||||
// Clear feedback when hovering stops
|
||||
if (event.detail.phase === 'end') {
|
||||
span.textContent = '';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.detect-hover span {
|
||||
position: relative;
|
||||
top: -4px;
|
||||
left: 8px;
|
||||
border-radius: var(--sl-border-radius-small);
|
||||
background: var(--sl-color-neutral-900);
|
||||
color: var(--sl-color-neutral-0);
|
||||
text-align: center;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.detect-hover span:empty {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { useState } from 'react';
|
||||
import { SlRating } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const terms = ['No rating', 'Terrible', 'Bad', 'OK', 'Good', 'Excellent'];
|
||||
const css = `
|
||||
.detect-hover span {
|
||||
position: relative;
|
||||
top: -4px;
|
||||
left: 8px;
|
||||
border-radius: var(--sl-border-radius-small);
|
||||
background: var(--sl-color-neutral-900);
|
||||
color: var(--sl-color-neutral-0);
|
||||
text-align: center;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.detect-hover span:empty {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
function handleHover(event) {
|
||||
rating.addEventListener('sl-hover', event => {
|
||||
setFeedback(terms[event.detail.value]);
|
||||
|
||||
// Clear feedback when hovering stops
|
||||
if (event.detail.phase === 'end') {
|
||||
setFeedback('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const App = () => {
|
||||
const [feedback, setFeedback] = useState(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="detect-hover">
|
||||
<SlRating label="Rating" onSlHover={handleHover} />
|
||||
<span>{feedback}</span>
|
||||
</div>
|
||||
<style>{css}</style>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Custom Icons
|
||||
|
||||
You can provide custom icons by passing a function to the `getSymbol` property.
|
||||
@@ -112,7 +205,6 @@ You can provide custom icons by passing a function to the `getSymbol` property.
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import '@shoelace-style/shoelace/dist/components/icon/icon';
|
||||
import { SlRating } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
@@ -142,7 +234,6 @@ You can also use the `getSymbol` property to render different icons based on val
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import '@shoelace-style/shoelace/dist/components/icon/icon';
|
||||
import { SlRating } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
function getSymbol(value) {
|
||||
|
||||
@@ -4,28 +4,26 @@
|
||||
|
||||
```html preview
|
||||
<sl-select>
|
||||
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item value="option-4">Option 4</sl-menu-item>
|
||||
<sl-menu-item value="option-5">Option 5</sl-menu-item>
|
||||
<sl-menu-item value="option-6">Option 6</sl-menu-item>
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
<sl-option value="option-4">Option 4</sl-option>
|
||||
<sl-option value="option-5">Option 5</sl-option>
|
||||
<sl-option value="option-6">Option 6</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlDivider, SlMenuItem, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect>
|
||||
<SlMenuItem value="option-1">Option 1</SlMenuItem>
|
||||
<SlMenuItem value="option-2">Option 2</SlMenuItem>
|
||||
<SlMenuItem value="option-3">Option 3</SlMenuItem>
|
||||
<SlDivider />
|
||||
<SlMenuItem value="option-4">Option 4</SlMenuItem>
|
||||
<SlMenuItem value="option-5">Option 5</SlMenuItem>
|
||||
<SlMenuItem value="option-6">Option 6</SlMenuItem>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
<SlOption value="option-4">Option 4</SlOption>
|
||||
<SlOption value="option-5">Option 5</SlOption>
|
||||
<SlOption value="option-6">Option 6</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
@@ -40,20 +38,20 @@ Use the `label` attribute to give the select an accessible label. For labels tha
|
||||
|
||||
```html preview
|
||||
<sl-select label="Select one">
|
||||
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlMenuItem, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect label="Select one">
|
||||
<SlMenuItem value="option-1">Option 1</SlMenuItem>
|
||||
<SlMenuItem value="option-2">Option 2</SlMenuItem>
|
||||
<SlMenuItem value="option-3">Option 3</SlMenuItem>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
@@ -64,20 +62,20 @@ Add descriptive help text to a select with the `help-text` attribute. For help t
|
||||
|
||||
```html preview
|
||||
<sl-select label="Experience" help-text="Please tell us your skill level.">
|
||||
<sl-menu-item value="1">Novice</sl-menu-item>
|
||||
<sl-menu-item value="2">Intermediate</sl-menu-item>
|
||||
<sl-menu-item value="3">Advanced</sl-menu-item>
|
||||
<sl-option value="1">Novice</sl-option>
|
||||
<sl-option value="2">Intermediate</sl-option>
|
||||
<sl-option value="3">Advanced</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlMenuItem, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect label="Experience" help-text="Please tell us your skill level.">
|
||||
<SlMenuItem value="1">Novice</SlMenuItem>
|
||||
<SlMenuItem value="2">Intermediate</SlMenuItem>
|
||||
<SlMenuItem value="3">Advanced</SlMenuItem>
|
||||
<SlOption value="1">Novice</SlOption>
|
||||
<SlOption value="2">Intermediate</SlOption>
|
||||
<SlOption value="3">Advanced</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
@@ -88,44 +86,44 @@ Use the `placeholder` attribute to add a placeholder.
|
||||
|
||||
```html preview
|
||||
<sl-select placeholder="Select one">
|
||||
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlMenuItem, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect placeholder="Select one">
|
||||
<SlMenuItem value="option-1">Option 1</SlMenuItem>
|
||||
<SlMenuItem value="option-2">Option 2</SlMenuItem>
|
||||
<SlMenuItem value="option-3">Option 3</SlMenuItem>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
### Clearable
|
||||
|
||||
Use the `clearable` attribute to make the control clearable.
|
||||
Use the `clearable` attribute to make the control clearable. The clear button only appears when an option is selected.
|
||||
|
||||
```html preview
|
||||
<sl-select placeholder="Clearable" clearable>
|
||||
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
<sl-select clearable value="option-1">
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlMenuItem, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect placeholder="Clearable" clearable>
|
||||
<SlMenuItem value="option-1">Option 1</SlMenuItem>
|
||||
<SlMenuItem value="option-2">Option 2</SlMenuItem>
|
||||
<SlMenuItem value="option-3">Option 3</SlMenuItem>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
@@ -136,20 +134,20 @@ Add the `filled` attribute to draw a filled select.
|
||||
|
||||
```html preview
|
||||
<sl-select filled>
|
||||
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlMenuItem, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect filled>
|
||||
<SlMenuItem value="option-1">Option 1</SlMenuItem>
|
||||
<SlMenuItem value="option-2">Option 2</SlMenuItem>
|
||||
<SlMenuItem value="option-3">Option 3</SlMenuItem>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
@@ -160,20 +158,20 @@ Use the `pill` attribute to give selects rounded edges.
|
||||
|
||||
```html preview
|
||||
<sl-select pill>
|
||||
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlMenuItem, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect pill>
|
||||
<SlMenuItem value="option-1">Option 1</SlMenuItem>
|
||||
<SlMenuItem value="option-2">Option 2</SlMenuItem>
|
||||
<SlMenuItem value="option-3">Option 3</SlMenuItem>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
@@ -184,227 +182,167 @@ Use the `disabled` attribute to disable a select.
|
||||
|
||||
```html preview
|
||||
<sl-select placeholder="Disabled" disabled>
|
||||
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlMenuItem, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect placeholder="Disabled" disabled>
|
||||
<SlMenuItem value="option-1">Option 1</SlMenuItem>
|
||||
<SlMenuItem value="option-2">Option 2</SlMenuItem>
|
||||
<SlMenuItem value="option-3">Option 3</SlMenuItem>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
### Setting the Selection
|
||||
|
||||
Use the `value` attribute to set the current selection. When users interact with the control, its `value` will update to reflect the newly selected menu item's value. Note that the value must be an array when using the [`multiple`](#multiple) option.
|
||||
|
||||
```html preview
|
||||
<sl-select value="option-2">
|
||||
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlDivider, SlMenuItem, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect value="option-2">
|
||||
<SlMenuItem value="option-1">Option 1</SlMenuItem>
|
||||
<SlMenuItem value="option-2">Option 2</SlMenuItem>
|
||||
<SlMenuItem value="option-3">Option 3</SlMenuItem>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
### Setting the Selection Imperatively
|
||||
|
||||
To programmatically set the selection, update the `value` property as shown below. Note that the value must be an array when using the [`multiple`](#multiple) option.
|
||||
|
||||
```html preview
|
||||
<div class="selecting-example">
|
||||
<sl-select>
|
||||
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
</sl-select>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-button data-option="option-1">Set 1</sl-button>
|
||||
<sl-button data-option="option-2">Set 2</sl-button>
|
||||
<sl-button data-option="option-3">Set 3</sl-button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const container = document.querySelector('.selecting-example');
|
||||
const select = container.querySelector('sl-select');
|
||||
|
||||
[...container.querySelectorAll('sl-button')].map(button => {
|
||||
button.addEventListener('click', () => {
|
||||
select.value = button.dataset.option;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { useState } from 'react';
|
||||
import { SlButton, SlMenuItem, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => {
|
||||
const [value, setValue] = useState('option-1');
|
||||
|
||||
return (
|
||||
<>
|
||||
<SlSelect value={value} onSlChange={event => setValue(event.target.value)}>
|
||||
<SlMenuItem value="option-1">Option 1</SlMenuItem>
|
||||
<SlMenuItem value="option-2">Option 2</SlMenuItem>
|
||||
<SlMenuItem value="option-3">Option 3</SlMenuItem>
|
||||
</SlSelect>
|
||||
|
||||
<br />
|
||||
|
||||
<SlButton onClick={() => setValue('option-1')}>Set 1</SlButton>
|
||||
<SlButton onClick={() => setValue('option-2')}>Set 2</SlButton>
|
||||
<SlButton onClick={() => setValue('option-3')}>Set 3</SlButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### Multiple
|
||||
|
||||
To allow multiple options to be selected, use the `multiple` attribute. With this option, `value` will be an array of strings instead of a string. It's a good practice to use `clearable` when this option is enabled.
|
||||
To allow multiple options to be selected, use the `multiple` attribute. It's a good practice to use `clearable` when this option is enabled. To set multiple values at once, set `value` to a space-delimited list of values.
|
||||
|
||||
```html preview
|
||||
<sl-select placeholder="Select a few" multiple clearable>
|
||||
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item value="option-4">Option 4</sl-menu-item>
|
||||
<sl-menu-item value="option-5">Option 5</sl-menu-item>
|
||||
<sl-menu-item value="option-6">Option 6</sl-menu-item>
|
||||
<sl-select label="Select a Few" value="option-1 option-2 option-3" multiple clearable>
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
<sl-option value="option-4">Option 4</sl-option>
|
||||
<sl-option value="option-5">Option 5</sl-option>
|
||||
<sl-option value="option-6">Option 6</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlDivider, SlMenuItem, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect placeholder="Select a few" multiple clearable>
|
||||
<SlMenuItem value="option-1">Option 1</SlMenuItem>
|
||||
<SlMenuItem value="option-2">Option 2</SlMenuItem>
|
||||
<SlMenuItem value="option-3">Option 3</SlMenuItem>
|
||||
<SlDivider />
|
||||
<SlMenuItem value="option-4">Option 4</SlMenuItem>
|
||||
<SlMenuItem value="option-5">Option 5</SlMenuItem>
|
||||
<SlMenuItem value="option-6">Option 6</SlMenuItem>
|
||||
<SlSelect label="Select a Few" value="option-1 option-2 option-3" multiple clearable>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
<SlOption value="option-4">Option 4</SlOption>
|
||||
<SlOption value="option-5">Option 5</SlOption>
|
||||
<SlOption value="option-6">Option 6</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
?> When using the `multiple` option, the value will be an array instead of a string. You may need to [set the selection imperatively](#setting-the-selection-imperatively) unless you're using a framework that supports binding properties declaratively.
|
||||
?> Note that multi-select options may wrap, causing the control to expand vertically. You can use the `max-options-visible` attribute to control the maximum number of selected options to show at once.
|
||||
|
||||
### Grouping Options
|
||||
### Setting Initial Values
|
||||
|
||||
Options can be grouped visually using menu labels and dividers.
|
||||
Use the `value` attribute to set the initial selection. When using `multiple`, use space-delimited values to select more than one option.
|
||||
|
||||
```html preview
|
||||
<sl-select placeholder="Select one">
|
||||
<sl-menu-label>Group 1</sl-menu-label>
|
||||
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-label>Group 2</sl-menu-label>
|
||||
<sl-menu-item value="option-4">Option 4</sl-menu-item>
|
||||
<sl-menu-item value="option-5">Option 5</sl-menu-item>
|
||||
<sl-menu-item value="option-6">Option 6</sl-menu-item>
|
||||
<sl-select value="option-1 option-2" multiple clearable>
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
<sl-option value="option-4">Option 4</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlDivider, SlMenuItem, SlMenuLabel, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
import { SlDivider, SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect placeholder="Select one">
|
||||
<SlMenuLabel>Group 1</SlMenuLabel>
|
||||
<SlMenuItem value="option-1">Option 1</SlMenuItem>
|
||||
<SlMenuItem value="option-2">Option 2</SlMenuItem>
|
||||
<SlMenuItem value="option-3">Option 3</SlMenuItem>
|
||||
<SlDivider></SlDivider>
|
||||
<SlMenuLabel>Group 2</SlMenuLabel>
|
||||
<SlMenuItem value="option-4">Option 4</SlMenuItem>
|
||||
<SlMenuItem value="option-5">Option 5</SlMenuItem>
|
||||
<SlMenuItem value="option-6">Option 6</SlMenuItem>
|
||||
<SlSelect value="option-1 option-2" multiple clearable>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
### Grouping Options
|
||||
|
||||
Use `<sl-divider>` to group listbox items visually. You can also use `<small>` to provide labels, but they won't be announced by most assistive devices.
|
||||
|
||||
```html preview
|
||||
<sl-select>
|
||||
<small>Section 1</small>
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
<sl-divider></sl-divider>
|
||||
<small>Section 2</small>
|
||||
<sl-option value="option-4">Option 4</sl-option>
|
||||
<sl-option value="option-5">Option 5</sl-option>
|
||||
<sl-option value="option-6">Option 6</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
<SlOption value="option-4">Option 4</SlOption>
|
||||
<SlOption value="option-5">Option 5</SlOption>
|
||||
<SlOption value="option-6">Option 6</SlOption>
|
||||
</SlSelect>
|
||||
);
|
||||
```
|
||||
|
||||
### Sizes
|
||||
|
||||
Use the `size` attribute to change a select's size.
|
||||
Use the `size` attribute to change a select's size. Note that size does not apply to listbox options.
|
||||
|
||||
```html preview
|
||||
<sl-select placeholder="Small" size="small" multiple>
|
||||
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
<sl-select placeholder="Small" size="small">
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-select placeholder="Medium" size="medium" multiple>
|
||||
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
<sl-select placeholder="Medium" size="medium">
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
|
||||
<br />
|
||||
|
||||
<sl-select placeholder="Large" size="large" multiple>
|
||||
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
<sl-select placeholder="Large" size="large">
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlMenuItem, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlSelect placeholder="Small" size="small" multiple>
|
||||
<SlMenuItem value="option-1">Option 1</SlMenuItem>
|
||||
<SlMenuItem value="option-2">Option 2</SlMenuItem>
|
||||
<SlMenuItem value="option-3">Option 3</SlMenuItem>
|
||||
<SlSelect placeholder="Small" size="small">
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
|
||||
<br />
|
||||
|
||||
<SlSelect placeholder="Medium" size="medium" multiple>
|
||||
<SlMenuItem value="option-1">Option 1</SlMenuItem>
|
||||
<SlMenuItem value="option-2">Option 2</SlMenuItem>
|
||||
<SlMenuItem value="option-3">Option 3</SlMenuItem>
|
||||
<SlSelect placeholder="Medium" size="medium">
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
|
||||
<br />
|
||||
|
||||
<SlSelect placeholder="Large" size="large" multiple>
|
||||
<SlMenuItem value="option-1">Option 1</SlMenuItem>
|
||||
<SlMenuItem value="option-2">Option 2</SlMenuItem>
|
||||
<SlMenuItem value="option-3">Option 3</SlMenuItem>
|
||||
<SlSelect placeholder="Large" size="large">
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
</>
|
||||
);
|
||||
@@ -412,88 +350,82 @@ const App = () => (
|
||||
|
||||
### Placement
|
||||
|
||||
The preferred placement of the select's menu can be set with the `placement` attribute. Note that the actual position may vary to ensure the panel remains in the viewport. Valid placements are `top` and `bottom`.
|
||||
The preferred placement of the select's listbox can be set with the `placement` attribute. Note that the actual position may vary to ensure the panel remains in the viewport. Valid placements are `top` and `bottom`.
|
||||
|
||||
```html preview
|
||||
<sl-select placement="top">
|
||||
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import {
|
||||
SlMenuItem,
|
||||
SlOption,
|
||||
SlSelect
|
||||
} from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSelect placement="top">
|
||||
<SlMenuItem value="option-1">Option 1</SlMenuItem>
|
||||
<SlMenuItem value="option-2">Option 2</SlMenuItem>
|
||||
<SlMenuItem value="option-3">Option 3</SlMenuItem>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlDropdown>
|
||||
);
|
||||
```
|
||||
|
||||
### Prefix & Suffix Icons
|
||||
### Prefix Icons
|
||||
|
||||
Use the `prefix` and `suffix` slots to add icons.
|
||||
Use the `prefix` slot to prepend an icon to the control.
|
||||
|
||||
```html preview
|
||||
<sl-select placeholder="Small" size="small">
|
||||
<sl-select placeholder="Small" size="small" clearable>
|
||||
<sl-icon name="house" slot="prefix"></sl-icon>
|
||||
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
<sl-icon name="chat" slot="suffix"></sl-icon>
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
<br />
|
||||
<sl-select placeholder="Medium" size="medium">
|
||||
<sl-select placeholder="Medium" size="medium" clearable>
|
||||
<sl-icon name="house" slot="prefix"></sl-icon>
|
||||
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
<sl-icon name="chat" slot="suffix"></sl-icon>
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
<br />
|
||||
<sl-select placeholder="Large" size="large">
|
||||
<sl-select placeholder="Large" size="large" clearable>
|
||||
<sl-icon name="house" slot="prefix"></sl-icon>
|
||||
<sl-menu-item value="option-1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="option-2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="option-3">Option 3</sl-menu-item>
|
||||
<sl-icon name="chat" slot="suffix"></sl-icon>
|
||||
<sl-option value="option-1">Option 1</sl-option>
|
||||
<sl-option value="option-2">Option 2</sl-option>
|
||||
<sl-option value="option-3">Option 3</sl-option>
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlIcon, SlMenuItem, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
import { SlIcon, SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlSelect placeholder="Small" size="small">
|
||||
<SlIcon name="house" slot="prefix"></SlIcon>
|
||||
<SlMenuItem value="option-1">Option 1</SlMenuItem>
|
||||
<SlMenuItem value="option-2">Option 2</SlMenuItem>
|
||||
<SlMenuItem value="option-3">Option 3</SlMenuItem>
|
||||
<SlIcon name="chat" slot="suffix"></SlIcon>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
<br />
|
||||
<SlSelect placeholder="Medium" size="medium">
|
||||
<SlIcon name="house" slot="prefix"></SlIcon>
|
||||
<SlMenuItem value="option-1">Option 1</SlMenuItem>
|
||||
<SlMenuItem value="option-2">Option 2</SlMenuItem>
|
||||
<SlMenuItem value="option-3">Option 3</SlMenuItem>
|
||||
<SlIcon name="chat" slot="suffix"></SlIcon>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
<br />
|
||||
<SlSelect placeholder="Large" size="large">
|
||||
<SlIcon name="house" slot="prefix"></SlIcon>
|
||||
<SlMenuItem value="option-1">Option 1</SlMenuItem>
|
||||
<SlMenuItem value="option-2">Option 2</SlMenuItem>
|
||||
<SlMenuItem value="option-3">Option 3</SlMenuItem>
|
||||
<SlIcon name="chat" slot="suffix"></SlIcon>
|
||||
<SlOption value="option-1">Option 1</SlOption>
|
||||
<SlOption value="option-2">Option 2</SlOption>
|
||||
<SlOption value="option-3">Option 3</SlOption>
|
||||
</SlSelect>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -379,9 +379,9 @@ Try resizing the example below with each option and notice how the panels respon
|
||||
</sl-split-panel>
|
||||
|
||||
<sl-select label="Primary Panel" value="" style="max-width: 200px; margin-top: 1rem;">
|
||||
<sl-menu-item value="">None</sl-menu-item>
|
||||
<sl-menu-item value="start">Start</sl-menu-item>
|
||||
<sl-menu-item value="end">End</sl-menu-item>
|
||||
<sl-option value="">None</sl-option>
|
||||
<sl-option value="start">Start</sl-option>
|
||||
<sl-option value="end">End</sl-option>
|
||||
</sl-select>
|
||||
</div>
|
||||
|
||||
@@ -583,11 +583,11 @@ const App = () => (
|
||||
|
||||
### Customizing the Divider
|
||||
|
||||
You can target the `divider` part to apply CSS properties to the divider. To add a handle, slot an icon or another element into the `handle` slot. When customizing the divider, make sure to think about focus styles for keyboard users.
|
||||
You can target the `divider` part to apply CSS properties to the divider. To add a custom handle, slot an icon into the `divider` slot. When customizing the divider, make sure to think about focus styles for keyboard users.
|
||||
|
||||
```html preview
|
||||
<sl-split-panel style="--divider-width: 20px;">
|
||||
<sl-icon slot="handle" name="grip-vertical"></sl-icon>
|
||||
<sl-icon slot="divider" name="grip-vertical"></sl-icon>
|
||||
<div
|
||||
slot="start"
|
||||
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center;"
|
||||
@@ -608,7 +608,7 @@ import { SlSplitPanel, SlIcon } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<SlSplitPanel style={{ '--divider-width': '20px' }}>
|
||||
<SlIcon slot="handle" name="grip-vertical" />
|
||||
<SlIcon slot="divider" name="grip-vertical" />
|
||||
<div
|
||||
slot="start"
|
||||
style={{
|
||||
@@ -640,9 +640,9 @@ const App = () => (
|
||||
Here's a more elaborate example that changes the divider's color and width and adds a styled handle.
|
||||
|
||||
```html preview
|
||||
<div class="split-panel-handle">
|
||||
<div class="split-panel-divider">
|
||||
<sl-split-panel>
|
||||
<sl-icon slot="handle" name="grip-vertical"></sl-icon>
|
||||
<sl-icon slot="divider" name="grip-vertical"></sl-icon>
|
||||
<div
|
||||
slot="start"
|
||||
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center;"
|
||||
@@ -659,15 +659,15 @@ Here's a more elaborate example that changes the divider's color and width and a
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.split-panel-handle sl-split-panel {
|
||||
.split-panel-divider sl-split-panel {
|
||||
--divider-width: 2px;
|
||||
}
|
||||
|
||||
.split-panel-handle sl-split-panel::part(divider) {
|
||||
.split-panel-divider sl-split-panel::part(divider) {
|
||||
background-color: var(--sl-color-pink-600);
|
||||
}
|
||||
|
||||
.split-panel-handle sl-icon {
|
||||
.split-panel-divider sl-icon {
|
||||
position: absolute;
|
||||
border-radius: var(--sl-border-radius-small);
|
||||
background: var(--sl-color-pink-600);
|
||||
@@ -675,11 +675,11 @@ Here's a more elaborate example that changes the divider's color and width and a
|
||||
padding: 0.5rem 0.125rem;
|
||||
}
|
||||
|
||||
.split-panel-handle sl-split-panel::part(divider):focus-visible {
|
||||
.split-panel-divider sl-split-panel::part(divider):focus-visible {
|
||||
background-color: var(--sl-color-primary-600);
|
||||
}
|
||||
|
||||
.split-panel-handle sl-split-panel:focus-within sl-icon {
|
||||
.split-panel-divider sl-split-panel:focus-within sl-icon {
|
||||
background-color: var(--sl-color-primary-600);
|
||||
color: var(--sl-color-neutral-0);
|
||||
}
|
||||
@@ -690,15 +690,15 @@ Here's a more elaborate example that changes the divider's color and width and a
|
||||
import { SlSplitPanel, SlIcon } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const css = `
|
||||
.split-panel-handle sl-split-panel {
|
||||
.split-panel-divider sl-split-panel {
|
||||
--divider-width: 2px;
|
||||
}
|
||||
|
||||
.split-panel-handle sl-split-panel::part(divider) {
|
||||
.split-panel-divider sl-split-panel::part(divider) {
|
||||
background-color: var(--sl-color-pink-600);
|
||||
}
|
||||
|
||||
.split-panel-handle sl-icon {
|
||||
.split-panel-divider sl-icon {
|
||||
position: absolute;
|
||||
border-radius: var(--sl-border-radius-small);
|
||||
background: var(--sl-color-pink-600);
|
||||
@@ -706,11 +706,11 @@ const css = `
|
||||
padding: .5rem .125rem;
|
||||
}
|
||||
|
||||
.split-panel-handle sl-split-panel::part(divider):focus-visible {
|
||||
.split-panel-divider sl-split-panel::part(divider):focus-visible {
|
||||
background-color: var(--sl-color-primary-600);
|
||||
}
|
||||
|
||||
.split-panel-handle sl-split-panel:focus-within sl-icon {
|
||||
.split-panel-divider sl-split-panel:focus-within sl-icon {
|
||||
background-color: var(--sl-color-primary-600);
|
||||
color: var(--sl-color-neutral-0);
|
||||
}
|
||||
@@ -718,9 +718,9 @@ const css = `
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<div className="split-panel-handle">
|
||||
<div className="split-panel-divider">
|
||||
<SlSplitPanel>
|
||||
<SlIcon slot="handle" name="grip-vertical" />
|
||||
<SlIcon slot="divider" name="grip-vertical" />
|
||||
<div
|
||||
slot="start"
|
||||
style={{
|
||||
|
||||
@@ -44,12 +44,38 @@ import { SlSwitch } from '@shoelace-style/shoelace/dist/react';
|
||||
const App = () => <SlSwitch disabled>Disabled</SlSwitch>;
|
||||
```
|
||||
|
||||
### Custom Size
|
||||
## Sizes
|
||||
|
||||
Use the available custom properties to make the switch a different size.
|
||||
Use the `size` attribute to change a switch's size.
|
||||
|
||||
```html preview
|
||||
<sl-switch style="--width: 80px; --height: 32px; --thumb-size: 26px;">Really big</sl-switch>
|
||||
<sl-switch size="small">Small</sl-switch>
|
||||
<br />
|
||||
<sl-switch size="medium">Medium</sl-switch>
|
||||
<br />
|
||||
<sl-switch size="large">Large</sl-switch>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
import { SlSwitch } from '@shoelace-style/shoelace/dist/react';
|
||||
|
||||
const App = () => (
|
||||
<>
|
||||
<SlSwitch size="small">Small</SlSwitch>
|
||||
<br />
|
||||
<SlSwitch size="medium">Medium</SlSwitch>
|
||||
<br />
|
||||
<SlSwitch size="large">Large</SlSwitch>
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
### Custom Styles
|
||||
|
||||
Use the available custom properties to change how the switch is styled.
|
||||
|
||||
```html preview
|
||||
<sl-switch style="--width: 80px; --height: 40px; --thumb-size: 36px;">Really big</sl-switch>
|
||||
```
|
||||
|
||||
```jsx react
|
||||
|
||||
@@ -79,9 +79,9 @@ The `selection` attribute lets you change the selection behavior of the tree.
|
||||
|
||||
```html preview
|
||||
<sl-select id="selection-mode" value="single" label="Selection">
|
||||
<sl-menu-item value="single">Single</sl-menu-item>
|
||||
<sl-menu-item value="multiple">Multiple</sl-menu-item>
|
||||
<sl-menu-item value="leaf">Leaf</sl-menu-item>
|
||||
<sl-option value="single">Single</sl-option>
|
||||
<sl-option value="multiple">Multiple</sl-option>
|
||||
<sl-option value="leaf">Leaf</sl-option>
|
||||
</sl-select>
|
||||
|
||||
<br />
|
||||
@@ -287,12 +287,12 @@ const App = () => {
|
||||
};
|
||||
```
|
||||
|
||||
### Custom expand/collapse icons
|
||||
### Customizing the Expand and Collapse Icons
|
||||
|
||||
Use the `expand-icon` and `collapse-icon` slots to change the expand and collapse icons, respectively.
|
||||
Use the `expand-icon` and `collapse-icon` slots to change the expand and collapse icons, respectively. To disable the animation, override the `rotate` property on the `expand-button` part as shown below.
|
||||
|
||||
```html preview
|
||||
<sl-tree>
|
||||
<sl-tree class="custom-icons">
|
||||
<sl-icon name="plus-square" slot="expand-icon"></sl-icon>
|
||||
<sl-icon name="dash-square" slot="collapse-icon"></sl-icon>
|
||||
|
||||
@@ -322,6 +322,13 @@ Use the `expand-icon` and `collapse-icon` slots to change the expand and collaps
|
||||
<sl-tree-item>Fern</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
</sl-tree>
|
||||
|
||||
<style>
|
||||
.custom-icons sl-tree-item::part(expand-button) {
|
||||
/* Disable the expand/collapse animation */
|
||||
rotate: none;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
@@ -398,11 +405,11 @@ Decorative icons can be used before labels to provide hints for each node.
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>
|
||||
<sl-icon name="file-pdf"></sl-icon>
|
||||
final.pdg
|
||||
final.pdf
|
||||
</sl-tree-item>
|
||||
<sl-tree-item>
|
||||
<sl-icon name="file-bar-graph"></sl-icon>
|
||||
sales.txt
|
||||
sales.xls
|
||||
</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
</sl-tree-item>
|
||||
|
||||
@@ -7,7 +7,7 @@ According to [The A11Y Project](https://www.a11yproject.com/posts/2013-01-11-how
|
||||
Since visually hidden content can receive focus when tabbing, the element will become visible when something inside receives focus. This behavior is intentional, as sighted keyboards user won't be able to determine where the focus indicator is without it.
|
||||
|
||||
```html preview
|
||||
<div style="min-height: 100px;">
|
||||
<div style="min-height: 1.875rem;">
|
||||
<sl-visually-hidden>
|
||||
<a href="#">Skip to main content</a>
|
||||
</sl-visually-hidden>
|
||||
@@ -30,7 +30,7 @@ In this example, the link will open a new window. Screen readers will announce "
|
||||
|
||||
### Content Conveyed By Context
|
||||
|
||||
Adding a title or label may seem redundant at times, but they're very helpful for unsighted users. Rather than omit them, you can provide context to unsighted users with visually hidden content.
|
||||
Adding a label may seem redundant at times, but they're very helpful for unsighted users. Rather than omit them, you can provide context to unsighted users with visually hidden content that will be announced by assistive devices such as screen readers.
|
||||
|
||||
```html preview
|
||||
<sl-card style="width: 100%; max-width: 360px;">
|
||||
|
||||
@@ -6,8 +6,6 @@ Shoelace solves this problem by using the [`formdata`](https://developer.mozilla
|
||||
|
||||
?> Shoelace uses event listeners to intercept the form's `formdata` and `submit` events. This allows it to inject data and trigger validation as necessary. If you're also attaching an event listener to the form, _you must attach it after Shoelace form controls are connected to the DOM_, otherwise your logic will run before Shoelace has a chance to inject form data and validate form controls.
|
||||
|
||||
?> If you're using an older browser that doesn't support the `formdata` event, a lightweight polyfill will be automatically applied to ensure forms submit as expected.
|
||||
|
||||
## Data Serialization
|
||||
|
||||
Serialization is just a fancy word for collecting form data. If you're relying on standard form submissions, e.g. `<form action="...">`, you can probably skip this section. However, most modern apps use the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) or a library such as [axios](https://github.com/axios/axios) to submit forms using JavaScript.
|
||||
@@ -55,10 +53,10 @@ The form will not be submitted if a required field is incomplete.
|
||||
<sl-input name="name" label="Name" required></sl-input>
|
||||
<br />
|
||||
<sl-select label="Favorite Animal" clearable required>
|
||||
<sl-menu-item value="birds">Birds</sl-menu-item>
|
||||
<sl-menu-item value="cats">Cats</sl-menu-item>
|
||||
<sl-menu-item value="dogs">Dogs</sl-menu-item>
|
||||
<sl-menu-item value="other">Other</sl-menu-item>
|
||||
<sl-option value="birds">Birds</sl-option>
|
||||
<sl-option value="cats">Cats</sl-option>
|
||||
<sl-option value="dogs">Dogs</sl-option>
|
||||
<sl-option value="other">Other</sl-option>
|
||||
</sl-select>
|
||||
<br />
|
||||
<sl-textarea name="comment" label="Comment" required></sl-textarea>
|
||||
@@ -298,10 +296,10 @@ This example demonstrates custom validation styles using `data-user-invalid` and
|
||||
></sl-input>
|
||||
|
||||
<sl-select label="Favorite Animal" help-text="Select the best option." clearable required>
|
||||
<sl-menu-item value="birds">Birds</sl-menu-item>
|
||||
<sl-menu-item value="cats">Cats</sl-menu-item>
|
||||
<sl-menu-item value="dogs">Dogs</sl-menu-item>
|
||||
<sl-menu-item value="other">Other</sl-menu-item>
|
||||
<sl-option value="birds">Birds</sl-option>
|
||||
<sl-option value="cats">Cats</sl-option>
|
||||
<sl-option value="dogs">Dogs</sl-option>
|
||||
<sl-option value="other">Other</sl-option>
|
||||
</sl-select>
|
||||
|
||||
<sl-button type="submit" variant="primary">Submit</sl-button>
|
||||
@@ -324,7 +322,7 @@ This example demonstrates custom validation styles using `data-user-invalid` and
|
||||
|
||||
/* user invalid styles */
|
||||
.validity-styles sl-input[data-user-invalid]::part(base),
|
||||
.validity-styles sl-select[data-user-invalid]::part(control) {
|
||||
.validity-styles sl-select[data-user-invalid]::part(combobox) {
|
||||
border-color: var(--sl-color-danger-600);
|
||||
}
|
||||
|
||||
@@ -334,14 +332,14 @@ This example demonstrates custom validation styles using `data-user-invalid` and
|
||||
}
|
||||
|
||||
.validity-styles sl-input:focus-within[data-user-invalid]::part(base),
|
||||
.validity-styles sl-select:focus-within[data-user-invalid]::part(control) {
|
||||
.validity-styles sl-select:focus-within[data-user-invalid]::part(combobox) {
|
||||
border-color: var(--sl-color-danger-600);
|
||||
box-shadow: 0 0 0 var(--sl-focus-ring-width) var(--sl-color-danger-300);
|
||||
}
|
||||
|
||||
/* User valid styles */
|
||||
.validity-styles sl-input[data-user-valid]::part(base),
|
||||
.validity-styles sl-select[data-user-valid]::part(control) {
|
||||
.validity-styles sl-select[data-user-valid]::part(combobox) {
|
||||
border-color: var(--sl-color-success-600);
|
||||
}
|
||||
|
||||
@@ -351,9 +349,24 @@ This example demonstrates custom validation styles using `data-user-invalid` and
|
||||
}
|
||||
|
||||
.validity-styles sl-input:focus-within[data-user-valid]::part(base),
|
||||
.validity-styles sl-select:focus-within[data-user-valid]::part(control) {
|
||||
.validity-styles sl-select:focus-within[data-user-valid]::part(combobox) {
|
||||
border-color: var(--sl-color-success-600);
|
||||
box-shadow: 0 0 0 var(--sl-focus-ring-width) var(--sl-color-success-300);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Getting Associated Form Controls
|
||||
|
||||
At this time, using [`HTMLFormElement.elements`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/elements) will not return Shoelace form controls because the browser is unaware of their status as custom element form controls. Fortunately, Shoelace provides an `elements()` function that does something very similar. However, instead of returning an [`HTMLFormControlsCollection`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormControlsCollection), it returns an array of HTML and Shoelace form controls in the order they appear in the DOM.
|
||||
|
||||
```js
|
||||
import { getFormControls } from '@shoelace-style/shoelace/dist/utilities/form.js';
|
||||
|
||||
const form = document.querySelector('#my-form');
|
||||
const formControls = getFormControls(form);
|
||||
|
||||
console.log(formControls); // e.g. [input, sl-input, ...]
|
||||
```
|
||||
|
||||
?> You probably don't need this function! If you're gathering form data for submission, you probably want to use [Data Serialization](#data-serializing) instead.
|
||||
|
||||
@@ -4,7 +4,7 @@ Shoelace components are just regular HTML elements, or [custom elements](https:/
|
||||
|
||||
If you're new to custom elements, often referred to as "web components," this section will familiarize you with how to use them.
|
||||
|
||||
## Properties
|
||||
## Attributes & Properties
|
||||
|
||||
Many components have properties that can be set using attributes. For example, buttons accept a `size` attribute that maps to the `size` property which dictates the button's size.
|
||||
|
||||
@@ -18,7 +18,7 @@ Some properties are boolean, so they only have true/false values. To activate a
|
||||
<sl-button disabled>Click me</sl-button>
|
||||
```
|
||||
|
||||
In rare cases, a property may require an array, an object, or a function. For example, to customize the color picker's list of preset swatches, you set the `swatches` property to an array of colors. This can be done with JavaScript.
|
||||
In rare cases, a property may require an array, an object, or a function. For example, to customize the color picker's list of preset swatches, you set the `swatches` property to an array of colors. This must be done with JavaScript.
|
||||
|
||||
```html
|
||||
<sl-color-picker></sl-color-picker>
|
||||
@@ -149,15 +149,45 @@ A clever way to use this method is to hide the `<body>` with `opacity: 0` and ad
|
||||
</script>
|
||||
```
|
||||
|
||||
## Component Rendering and Updating
|
||||
|
||||
Shoelace components are built with [Lit](https://lit.dev/), a tiny library that makes authoring custom elements easier, more maintainable, and a lot of fun! As a Shoelace user, here is some helpful information about rendering and updating you should probably be aware of.
|
||||
|
||||
To optimize performance and reduce re-renders, Lit batches component updates. This means changing multiple attributes or properties at the same time will result in just a single re-render. In most cases, this isn't an issue, but there may be times you'll need to wait for the component to update before continuing.
|
||||
|
||||
Consider this example. We're going to change the `checked` property of the checkbox and observe its corresponding `checked` attribute, which happens to reflect.
|
||||
|
||||
```js
|
||||
const checkbox = document.querySelector('sl-checkbox');
|
||||
checkbox.checked = true;
|
||||
|
||||
console.log(checkbox.hasAttribute('checked')); // false
|
||||
```
|
||||
|
||||
Most devs will expect this to be `true` instead of `false`, but the component hasn't had a chance to re-render yet so the attribute doesn't exist when `hasAttribute()` is called. Since changes are batched, we need to wait for the update before proceeding. This can be done using the `updateComplete` property, which is available on all Lit-based components.
|
||||
|
||||
```js
|
||||
const checkbox = document.querySelector('sl-checkbox');
|
||||
checkbox.checked = true;
|
||||
|
||||
checkbox.updateComplete.then(() => {
|
||||
console.log(checkbox.hasAttribute('checked')); // true
|
||||
});
|
||||
```
|
||||
|
||||
This time we see an empty string, which means the boolean attribute is now present!
|
||||
|
||||
?> Avoid using `setTimeout()` or `requestAnimationFrame()` in situations like this. They might work, but it's far more reliable to use `updateComplete` instead.
|
||||
|
||||
## Code Completion
|
||||
|
||||
### VS Code
|
||||
|
||||
Shoelace ships with a file called `vscode.html-custom-data.json` that can be used to describe its components to Visual Studio Code. This enables code completion for Shoelace components (also known as "code hinting" or "IntelliSense"). To enable it, you need to tell VS Code where the file is.
|
||||
Shoelace ships with a file called `vscode.html-custom-data.json` that can be used to describe it's custom elements to Visual Studio Code. This enables code completion for Shoelace components (also known as "code hinting" or "IntelliSense"). To enable it, you need to tell VS Code where the file is.
|
||||
|
||||
1. [Install Shoelace locally](/getting-started/installation#local-installation)
|
||||
2. Create a folder called `.vscode` at the root of your project
|
||||
3. Create a file inside the folder called `settings.json`
|
||||
2. If it doesn't already exist, create a folder called `.vscode` at the root of your project
|
||||
3. If it doesn't already exist, create a file inside that folder called `settings.json`
|
||||
4. Add the following to the file
|
||||
|
||||
```js
|
||||
|
||||
@@ -6,7 +6,143 @@ Components with the <sl-badge variant="warning" pill>Experimental</sl-badge> bad
|
||||
|
||||
New versions of Shoelace are released as-needed and generally occur when a critical mass of changes have accumulated. At any time, you can see what's coming in the next release by visiting [next.shoelace.style](https://next.shoelace.style).
|
||||
|
||||
?> During the beta period, these restrictions may be relaxed in the event of a mission-critical bug. 🐛
|
||||
## 2.0.0
|
||||
|
||||
This is the first stable release of Shoelace 2, meaning breaking changes to the API will no longer be accepted for this version. Development of Shoelace 2.0 started in January 2020. The first beta was released on [July 15, 2020](https://github.com/shoelace-style/shoelace/releases/tag/v2.0.0-beta.1). Since then, Shoelace has grown quite a bit! Here are some stats from the project as of January 24, 2023:
|
||||
|
||||
- 55 components have been built
|
||||
- [Over 2,500 commits](https://github.com/shoelace-style/shoelace/commits/next) have been made to the project
|
||||
- [85 people](https://github.com/shoelace-style/shoelace/graphs/contributors) have contributed to the project
|
||||
- [669 issues](https://github.com/shoelace-style/shoelace/issues?q=is%3Aissue+is%3Aclosed) have been filed on GitHub
|
||||
- [274 pull requests](https://github.com/shoelace-style/shoelace/pulls) have been opened
|
||||
- [More than 150 discussions](https://github.com/shoelace-style/shoelace/discussions) have been started on GitHub
|
||||
- [Over 500 people](https://discord.com/invite/mg8f26C) have joined the Shoelace community on Discord
|
||||
- [Over 300 million CDN hits](https://www.jsdelivr.com/package/npm/@shoelace-style/shoelace) per month
|
||||
- [Over 13,000 npm downloads](https://www.npmjs.com/package/@shoelace-style/shoelace) per week
|
||||
- [73rd most popular project](https://www.jsdelivr.com/statistics) on jsDelivr
|
||||
- [#2 product of the day](https://www.producthunt.com/products/shoelace-css) on Product Hunt (July 25, 2020)
|
||||
|
||||
I'd like to extend a very special thank you to every single contributor who worked to make this possible. Everyone who's filed a bug, submitted a PR, requested a feature, started a discussion, helped with testing, and advocated for the project. You are just as responsible for Shoelace's success as I am. I'd also like to thank the folks at [Font Awesome](https://fontawesome.com/) for recognizing Shoelace's potential and [believing in me](https://blog.fontawesome.com/shoelace-joins-font-awesome/) to make it happen.
|
||||
|
||||
Thank you! And keep building _awesome_ stuff!
|
||||
|
||||
Without further ado, here are the notes for this release.
|
||||
|
||||
- Added support for the `inert` attribute on `<sl-menu-item>` to allow hidden menu items to not accept focus [#1107](https://github.com/shoelace-style/shoelace/issues/1107)
|
||||
- Added the `tag` part to `<sl-select>`
|
||||
- Added `sl-hover` event to `<sl-rating>` [#1125](https://github.com/shoelace-style/shoelace/issues/1125)
|
||||
- Added the `@documentation` tag with a link to the docs for each component
|
||||
- Added the `form` attribute to all form controls to allow placing them outside of a `<form>` element [#1130](https://github.com/shoelace-style/shoelace/issues/1130)
|
||||
- Added the `getFormControls()` function as an alternative to `HTMLFormElement.elements`
|
||||
- Added missing docs for the `header-actions` slot in `<sl-dialog>` and `<sl-drawer>`
|
||||
- Added `hue-slider-handle` and `opacity-slider-handle` parts to `<sl-color-picker>` and correct other part names in the docs [#1142](https://github.com/shoelace-style/shoelace/issues/1142)
|
||||
- Fixed a bug in `<sl-select>` that prevented placeholders from showing when `multiple` was used [#1109](https://github.com/shoelace-style/shoelace/issues/1109)
|
||||
- Fixed a bug in `<sl-select>` that caused tags to not be rounded when using the `pill` attribute [#1117](https://github.com/shoelace-style/shoelace/issues/1117)
|
||||
- Fixed a bug in `<sl-select>` where the `sl-change` and `sl-input` events didn't weren't emitted when removing tags [#1119](https://github.com/shoelace-style/shoelace/issues/1119)
|
||||
- Fixed a bug in `<sl-select>` that caused the listbox to scroll to the first selected item when selecting multiple items [#1138](https://github.com/shoelace-style/shoelace/issues/1138)
|
||||
- Fixed a bug in `<sl-select>` where the input color and input hover color wasn't using the correct design tokens [#1143](https://github.com/shoelace-style/shoelace/issues/1143)
|
||||
- Fixed a bug in `<sl-color-picker>` that logged a console error when parsing swatches with whitespace
|
||||
- Fixed a bug in `<sl-color-picker>` that caused selected colors to be wrong due to incorrect HSV calculations
|
||||
- Fixed a bug in `<sl-color-picker>` that prevented the initial value from being set correct when assigned as a property [#1141](https://github.com/shoelace-style/shoelace/issues/1141)
|
||||
- Fixed a bug in `<sl-radio-button>` that caused the checked button's right border to be incorrect [#1110](https://github.com/shoelace-style/shoelace/issues/1110)
|
||||
- Fixed a bug in `<sl-spinner>` that caused the animation to stop working correctly in Safari [#1121](https://github.com/shoelace-style/shoelace/issues/1121)
|
||||
- Fixed a bug that prevented the entire `<sl-tab-panel>` to be hidden when inactive
|
||||
- Fixed a bug that caused the value of `<sl-radio-group>` to be `undefined` depending on where the radio was activated [#1134](https://github.com/shoelace-style/shoelace/issues/1134)
|
||||
- Fixed a bug that caused body content to shift when scroll locking was enabled [#1132](https://github.com/shoelace-style/shoelace/issues/1132)
|
||||
- Fixed a bug in `<sl-icon>` that caused icons to sometimes be clipped in Safari
|
||||
- Fixed a bug that prevented label colors from inheriting by default in `<sl-checkbox>`, `<sl-radio>`, and `<sl-switch>`
|
||||
- Fixed a bug in `<sl-radio-group>` that caused an extra margin between the host element and the internal fieldset [#1139](https://github.com/shoelace-style/shoelace/issues/1139)
|
||||
- Refactored the `ShoelaceFormControl` interface to remove the `invalid` property, allowing a more intuitive API for controlling validation internally
|
||||
- Renamed the internal `FormSubmitController` to `FormControlController` to better reflect what it's used for
|
||||
- Updated Lit to 2.6.1
|
||||
- Updated Floating UI to 1.1.0
|
||||
- Updated all other dependencies to latest versions
|
||||
|
||||
## 2.0.0-beta.88
|
||||
|
||||
This release includes a complete rewrite of `<sl-select>` to improve accessibility and simplify its internals.
|
||||
|
||||
- 🚨 BREAKING: rewrote `<sl-select>`
|
||||
- Accessibility has been significantly improved, especially in screen readers
|
||||
- You must use `<sl-option>` instead of `<sl-menu-item>` for options now
|
||||
- The `suffix` slot was removed because it was confusing to users and its position made the clear button inaccessible
|
||||
- The `max-tags-visible` attribute has been renamed to `max-options-visible`
|
||||
- Many parts have been removed or renamed (please see the docs for more details)
|
||||
- 🚨 BREAKING: removed the `sl-label-change` event from `<sl-menu-item>` (listen for `slotchange` instead)
|
||||
- 🚨 BREAKING: removed type to select logic from `<sl-menu>` (this was added specifically for `<sl-select>` which no longer uses `<sl-menu>`)
|
||||
- 🚨 BREAKING: swatches in `<sl-color-picker>` are no longer present by default (but you can set them using the `swatches` attribute now)
|
||||
- 🚨 BREAKING: improved the accessibility of `<sl-menu-item>` so checked items are announced as such
|
||||
- Checkbox menu items must now have `type="checkbox"` before applying the `checked` attribute
|
||||
- Checkbox menu items will now toggle their `checked` state on their own when selected
|
||||
- Disabled menu items will now receive focus, but are still not selectable
|
||||
- Added the `<sl-option>` component
|
||||
- Added Traditional Chinese translation [#1086](https://github.com/shoelace-style/shoelace/pull/1086)
|
||||
- Added support for `swatches` to be an attribute of `<sl-color-picker>` so swatches can be defined declaratively (it was previously a property; use a `;` to separate color values)
|
||||
- Fixed a bug in `<sl-tree-item>` where the checked/indeterminate states could get out of sync when using the `multiple` option [#1076](https://github.com/shoelace-style/shoelace/issues/1076)
|
||||
- Fixed a bug in `<sl-tree>` that caused `sl-selection-change` to emit before the DOM updated [#1096](https://github.com/shoelace-style/shoelace/issues/1096)
|
||||
- Fixed a bug that prevented `<sl-switch>` from submitting a default value of `on` when no value was provided [#1103](https://github.com/shoelace-style/shoelace/discussions/1103)
|
||||
- Fixed a bug in `<sl-textarea>` that caused the scrollbar to show sometimes when using `resize="auto"`
|
||||
- Fixed a bug in `<sl-input>` and `<sl-textarea>` that caused its validation states to be out of sync in some cases [#1063](https://github.com/shoelace-style/shoelace/issues/1063)
|
||||
- Reorganized all components to make class structures more consistent
|
||||
- Updated some incorrect default values for design tokens in the docs [#1097](https://github.com/shoelace-style/shoelace/issues/1097)
|
||||
- Updated non-public fields to use the `private` keyword (these were previously private only by convention, but now TypeScript will warn you)
|
||||
- Updated the hover style of `<sl-menu-item>` to be consistent with `<sl-option>`
|
||||
- Updated the status of `<sl-tree>` and `<sl-tree-item>` from experimental to stable
|
||||
- Updated React wrappers to use the latest API from `@lit-labs/react` [#1090](https://github.com/shoelace-style/shoelace/pull/1090)
|
||||
- Updated Bootstrap Icons to 1.10.3
|
||||
|
||||
## 2.0.0-beta.87
|
||||
|
||||
- 🚨 BREAKING: changed the default size of medium checkboxes, radios, and switches to 18px instead of 16px
|
||||
- 🚨 BREAKING: renamed the `--sl-toggle-size` design token to `--sl-toggle-size-medium`
|
||||
- Added the `--sl-toggle-size-small` and `--sl-toggle-size-large` design tokens
|
||||
- Added the `size` attribute to `<sl-checkbox>`, `<sl-radio>`, and `<sl-switch>` [#1071](https://github.com/shoelace-style/shoelace/issues/1071)
|
||||
- Added the `sl-input` event to `<sl-checkbox>`, `<sl-color-picker>`, `<sl-radio>`, `<sl-range>`, and `<sl-switch>`
|
||||
- Added HSV format to `<sl-color-picker>` [#1072](https://github.com/shoelace-style/shoelace/pull/1072)
|
||||
- Fixed a bug in `<sl-color-picker>` that sometimes prevented the color from updating when clicking or tapping on the controls
|
||||
- Fixed a bug in `<sl-color-picker>` that prevented text from being entered in the color input
|
||||
- Fixed a bug in `<sl-input>` that caused the `sl-change` event to be incorrectly emitted when the value was set programmatically [#917](https://github.com/shoelace-style/shoelace/issues/917)
|
||||
- Fixed a bug in `<sl-input>` and `<sl-textarea>` that made it impossible to disable spell checking [#1061](https://github.com/shoelace-style/shoelace/issues/1061)
|
||||
- Fixed non-modal behaviors in `<sl-drawer>` when using the `contained` attribute [#1051](https://github.com/shoelace-style/shoelace/issues/1051)
|
||||
- Fixed a bug in `<sl-checkbox>` and `<sl-radio>` that caused the checked icons to not scale property when resized
|
||||
- Fixed a bug that broke React imports [#1050](https://github.com/shoelace-style/shoelace/pull/1050)
|
||||
- Refactored `<sl-color-picker>` to use `@ctrl/tinycolor` instead of `color` saving ~67KB [#1072](https://github.com/shoelace-style/shoelace/pull/1072)
|
||||
- Removed the `formdata` event polyfill since it's now available in the last two versions of all major browsers
|
||||
|
||||
## 2.0.0-beta.86
|
||||
|
||||
- 🚨 BREAKING: changed the default value of `date` in `<sl-relative-time>` to the current date instead of the Unix epoch
|
||||
- 🚨 BREAKING: removed the `handle-icon` part and slot from `<sl-image-comparer>` (use `handle` instead)
|
||||
- 🚨 BREAKING: removed the `handle` slot from `<sl-split-panel>` (use the `divider` slot instead)
|
||||
- 🚨 BREAKING: removed the `--box-shadow` custom property from `<sl-alert>` (apply a box shadow to `::part(base)` instead)
|
||||
- 🚨 BREAKING: removed the `play-icon` and `pause-icon` parts (use the `play-icon` and `pause-icon` slots instead)
|
||||
- Added `header-actions` slot to `<sl-dialog>` and `<sl-drawer>`
|
||||
- Added the `expand-icon` and `collapse-icon` slots to `<sl-details>` and refactored the icon animation [#1046](https://github.com/shoelace-style/shoelace/discussions/1046)
|
||||
- Added the `play-icon` and `pause-icon` slots to `<sl-animated-image>` so you can customize the default icons
|
||||
- Converted `isTreeItem()` export to a static method of `<sl-tree-item>`
|
||||
- Fixed a bug in `<sl-tree-item>` where `sl-selection-change` was emitted when the selection didn't change [#1030](https://github.com/shoelace-style/shoelace/pull/1030)
|
||||
- Fixed a bug in `<sl-button-group>` that caused the border to render incorrectly when hovering over icons inside buttons [#1035](https://github.com/shoelace-style/shoelace/issues/1035)
|
||||
- Fixed an incorrect default for `flip-fallback-strategy` in `<sl-popup>` that caused the fallback strategy to be `initial` instead of `best-fit`, which is inconsistent with Floating UI's default [#1036](https://github.com/shoelace-style/shoelace/issues/1036)
|
||||
- Fixed a bug where browser validation tooltips would show up when hovering over form controls [#1037](https://github.com/shoelace-style/shoelace/issues/1037)
|
||||
- Fixed a bug in `<sl-tab-group>` that sometimes caused the active tab indicator to not animate
|
||||
- Fixed a bug in `<sl-tree-item>` that caused the expand/collapse icon slot to be out of sync when the node is open initially
|
||||
- Fixed the mislabeled `handle-icon` slot in `<sl-image-comparer>` (it now points to the `<slot>`, not the slot's fallback content)
|
||||
- Fixed the border radius in `<sl-dropdown>` so it matches with nested `<sl-menu>` elements
|
||||
- Fixed a bug that caused all button values to appear in submitted form data even if they weren't the submitter
|
||||
- Improved IntelliSense in VS Code, courtesy of [Burton's amazing CEM Analyzer plugin](https://github.com/break-stuff/cem-plugin-vs-code-custom-data-generator)
|
||||
- Improved accessibility of `<sl-alert>` so the alert is announced and the close button has a label
|
||||
- Improved accessibility of `<sl-progress-ring>` so slotted labels are announced along with visually hidden labels
|
||||
- Refactored all styles and animations to use `translate`, `rotate`, and `scale` instead of `transform`
|
||||
- Removed slot wrappers from many components, allowing better control over user-applied styles
|
||||
- Removed unused aria attributes from `<sl-skeleton>`
|
||||
- Replaced the `x` icon in the system icon library with `x-lg` to improve icon consistency
|
||||
|
||||
## 2.0.0-beta.85
|
||||
|
||||
- Fixed a bug in `<sl-dropdown>` that caused containing dialogs, drawers, etc. to close when pressing <kbd>Escape</kbd> while focused [#1024](https://github.com/shoelace-style/shoelace/issues/1024)
|
||||
- Fixed a bug in `<sl-tree-item>` that allowed lazy nodes to be incorrectly selected [#1023](https://github.com/shoelace-style/shoelace/pull/1023)
|
||||
- Fixed a typing bug in `<sl-tree-item>` [#1026](https://github.com/shoelace-style/shoelace/pull/1026)
|
||||
- Updated Floating UI to 1.0.7 to fix a bug that prevented `hoist` from working correctly in `<sl-dropdown>` after a recent update [#1024](https://github.com/shoelace-style/shoelace/issues/1024)
|
||||
|
||||
## 2.0.0-beta.84
|
||||
|
||||
|
||||
@@ -188,8 +188,6 @@ Please do not make any changes to `prettier.config.cjs` without consulting the m
|
||||
|
||||
Components should be composable, meaning you can easily reuse them with and within other components. This reduces the overall size of the library, expedites feature development, and maintains a consistent user experience.
|
||||
|
||||
The `<sl-select>` component, for example, makes use of the dropdown, input, menu, and menu item components. Because it's offloading most of its functionality and styles to lower-level components, the select component remains lightweight and its appearance is consistent with other form controls and menus.
|
||||
|
||||
### Component Structure
|
||||
|
||||
All components have a host element, which is a reference to the `<sl-*>` element itself. Make sure to always set the host element's `display` property to the appropriate value depending on your needs, as the default is `inline` per the custom element spec.
|
||||
@@ -202,6 +200,23 @@ All components have a host element, which is a reference to the `<sl-*>` element
|
||||
|
||||
Aside from `display`, avoid setting styles on the host element when possible. The reason for this is that styles applied to the host element are not encapsulated. Instead, create a base element that wraps the component's internals and style that instead. This convention also makes it easier to use BEM in components, as the base element can serve as the "block" entity.
|
||||
|
||||
When authoring components, please try to follow the same structure and conventions found in other components. Classes, for example, generally follow this structure:
|
||||
|
||||
- Static properties/methods
|
||||
- Private/public properties (that are _not_ reactive)
|
||||
- `@query` decorators
|
||||
- `@state` decorators
|
||||
- `@property` decorators
|
||||
- Lifecycle methods (`connectedCallback()`, `disconnectedCallback()`, `firstUpdated()`, etc.)
|
||||
- Private methods
|
||||
- `@watch` decorators
|
||||
- Public methods
|
||||
- The `render()` method
|
||||
|
||||
Please avoid using the `public` keyword for class fields. It's simply too verbose when combined with decorators, property names, and arguments. However, _please do_ add `private` in front of any property or method that is intended to be private.
|
||||
|
||||
?> This might seem like a lot, but it's fairly intuitive once you start working with the library. However, don't let this structure prevent you from submitting a PR. [Code can change](https://www.abeautifulsite.net/posts/code-can-change/) and nobody will chastise you for "getting it wrong." At the same time, encouraging consistency helps keep the library maintainable and easy for others to understand. (A lint rule that helps with things like this would be a very welcome PR!)
|
||||
|
||||
### Class Names
|
||||
|
||||
All components use a [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM), so styles are completely encapsulated from the rest of the document. As a result, class names used _inside_ a component won't conflict with class names _outside_ the component, so we're free to name them anything we want.
|
||||
@@ -224,12 +239,34 @@ When a component relies on the presence of slotted content to do something, don'
|
||||
|
||||
See the source of card, dialog, or drawer for examples.
|
||||
|
||||
### Dynamic Slot Names and Expand/Collapse Icons
|
||||
|
||||
A pattern has been established in `<sl-details>` and `<sl-tree-item>` for expand/collapse icons that animate on open/close. In short, create two slots called `expand-icon` and `collapse-icon` and render them both in the DOM, using CSS to show/hide only one based on the current open state. Avoid conditionally rendering them. Also avoid using dynamic slot names, such as `<slot name=${open ? 'open' : 'closed'}>`, because Firefox will not animate them.
|
||||
|
||||
There should be a container element immediately surrounding both slots. The container should be animated with CSS by default and it should have a part so the user can override the animation or disable it. Please refer to the source and documentation for `<sl-details>` and/or `<sl-tree-item>` for details.
|
||||
|
||||
### Fallback Content in Slots
|
||||
|
||||
When providing fallback content inside of `<slot>` elements, avoid adding parts, e.g.:
|
||||
|
||||
```html
|
||||
<slot name="icon">
|
||||
<sl-icon part="close-icon"></sl-icon>
|
||||
</slot>
|
||||
```
|
||||
|
||||
This creates confusion because the part will be documented, but it won't work when the user slots in their own content. The recommended way to customize this example is for the user to slot in their own content and target its styles with CSS as needed.
|
||||
|
||||
### Custom Events
|
||||
|
||||
Components must only emit custom events, and all custom events must start with `sl-` as a namespace. For compatibility with frameworks that utilize DOM templates, custom events must have lowercase, kebab-style names. For example, use `sl-change` instead of `slChange`.
|
||||
|
||||
This convention avoids the problem of browsers lowercasing attributes, causing some frameworks to be unable to listen to them. This problem isn't specific to one framework, but [Vue's documentation](https://vuejs.org/v2/guide/components-custom-events.html#Event-Names) provides a good explanation of the problem.
|
||||
|
||||
### Change Events
|
||||
|
||||
When change events are emitted by Shoelace components, they should be named `sl-change` and they should only be emitted as a result of user input. Programmatic changes, such as setting `el.value = '…'` _should not_ result in a change event being emitted. This is consistent with how native form controls work.
|
||||
|
||||
### CSS Custom Properties
|
||||
|
||||
To expose custom properties as part of a component's API, scope them to the `:host` block.
|
||||
@@ -276,7 +313,7 @@ This convention can be relaxed when the developer experience is greatly improved
|
||||
|
||||
### Naming CSS Parts
|
||||
|
||||
While CSS parts can be named [virtually anything](https://www.abeautifulsite.net/posts/valid-names-for-css-parts/), within Shoelace they must use the kebab-case convention and lowercase letters. Modifiers must be delimited by `--` like in BEM. This is useful for allowing users to target parts with various states, such as `my-part--focus`.
|
||||
While CSS parts can be named [virtually anything](https://www.abeautifulsite.net/posts/valid-names-for-css-parts/), within Shoelace they must use the kebab-case convention and lowercase letters. Additionally, [a BEM-inspired naming convention](https://www.abeautifulsite.net/posts/css-parts-inspired-by-bem/) is used to distinguish parts, subparts, and states.
|
||||
|
||||
When composing elements, use `part` to export the host element and `exportparts` to export its parts.
|
||||
|
||||
@@ -292,6 +329,20 @@ render() {
|
||||
|
||||
This results in a consistent, easy to understand structure for parts. In this example, the `icon` part will target the host element and the `icon__base` part will target the icon's `base` part.
|
||||
|
||||
### Dependencies
|
||||
|
||||
TL;DR – a component is a dependency if and only if it's rendered inside another component's shadow root.
|
||||
|
||||
Many Shoelace components use other Shoelace components internally. For example, `<sl-button>` uses both `<sl-icon>` and `<sl-spinner>` for its caret icon and loading state, respectively. Since these components appear in the button's shadow root, they are considered dependencies of Button. Since dependencies are automatically loaded, users only need to import the button and everything will work as expected.
|
||||
|
||||
Contrast this to `<sl-select>` and `<sl-option>`. At first, one might assume that Option is a dependency of Select. After all, you can't really use Select without slotting in at least one Option. However, Option _is not_ a dependency of Select! The reason is because no Option is rendered in the Select's shadow root. Since the options are provided by the user, it's up to them to import both components independently.
|
||||
|
||||
People often suggest that Shoelace should auto-load Select + Option, Menu + Menu Item, Breadcrumb + Breadcrumb Item, etc. Although some components are designed to work together, they're technically not dependencies so eagerly loading them may not be desirable. What if someone wants to roll their own component with a superset of features? They wouldn't be able to if Shoelace automatically imported it!
|
||||
|
||||
Similarly, in the case of `<sl-radio-group>` there was originally only `<sl-radio>`, but now you can use either `<sl-radio>` or `<sl-radio-button>` as child elements. Which component(s) should be auto-loaded dependencies in this case? Had Radio been a dependency of Radio Group, users that only wanted Radio Buttons would be forced to register both with no way to opt out and no way to provide their own customized version.
|
||||
|
||||
For non-dependencies, _the user_ should decide what gets registered, even if it comes with a minor inconvenience.
|
||||
|
||||
### Form Controls
|
||||
|
||||
Form controls should support submission and validation through the following conventions:
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
Border radius tokens are used to give sharp edges a more subtle, rounded effect. They use rem units so they scale with the base font size. The pixel values displayed are based on a 16px font size.
|
||||
|
||||
| Token | Value | Example |
|
||||
| ---------------------------- | -------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| `--sl-border-radius-small` | 0.125rem (2px) | <div class="border-radius-demo" style="border-radius: var(--sl-border-radius-small);"></div> |
|
||||
| `--sl-border-radius-medium` | 0.25rem (4px) | <div class="border-radius-demo" style="border-radius: var(--sl-border-radius-medium);"></div> |
|
||||
| `--sl-border-radius-large` | 0.5rem (8px) | <div class="border-radius-demo" style="border-radius: var(--sl-border-radius-large);"></div> |
|
||||
| `--sl-border-radius-x-large` | 1rem (16px) | <div class="border-radius-demo" style="border-radius: var(--sl-border-radius-x-large);"></div> |
|
||||
| `--sl-border-radius-circle` | 50% | <div class="border-radius-demo" style="border-radius: var(--sl-border-radius-circle);"></div> |
|
||||
| `--sl-border-radius-pill` | 9999px | <div class="border-radius-demo" style="border-radius: var(--sl-border-radius-pill); width: 6rem;"></div> |
|
||||
| Token | Value | Example |
|
||||
| ---------------------------- | --------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| `--sl-border-radius-small` | 0.1875rem (3px) | <div class="border-radius-demo" style="border-radius: var(--sl-border-radius-small);"></div> |
|
||||
| `--sl-border-radius-medium` | 0.25rem (4px) | <div class="border-radius-demo" style="border-radius: var(--sl-border-radius-medium);"></div> |
|
||||
| `--sl-border-radius-large` | 0.5rem (8px) | <div class="border-radius-demo" style="border-radius: var(--sl-border-radius-large);"></div> |
|
||||
| `--sl-border-radius-x-large` | 1rem (16px) | <div class="border-radius-demo" style="border-radius: var(--sl-border-radius-x-large);"></div> |
|
||||
| `--sl-border-radius-circle` | 50% | <div class="border-radius-demo" style="border-radius: var(--sl-border-radius-circle);"></div> |
|
||||
| `--sl-border-radius-pill` | 9999px | <div class="border-radius-demo" style="border-radius: var(--sl-border-radius-pill); width: 6rem;"></div> |
|
||||
|
||||
155
docs/tokens/more.md
Normal file
155
docs/tokens/more.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# More Design Tokens
|
||||
|
||||
All of the design tokens described herein are considered relatively stable. However, some changes might occur in future versions to address mission critical bugs or improvements. If such changes occur, they _will not_ be considered breaking changes and will be clearly documented in the [changelog](/resources/changelog).
|
||||
|
||||
Most design tokens are consistent across the light and dark theme. Those that vary will show both values.
|
||||
|
||||
?> Currently, the source of design tokens is considered to be [`light.css`](https://github.com/shoelace-style/shoelace/blob/next/src/themes/light.css). The dark theme, [dark.css](https://github.com/shoelace-style/shoelace/blob/next/src/themes/dark.css), mirrors all of the same tokens with dark mode-specific values where appropriate. Work is planned to move all design tokens to a single file, perhaps JSON or YAML, in the near future.
|
||||
|
||||
## Focus Rings
|
||||
|
||||
Focus ring tokens control the appearance of focus rings. Note that form inputs use `--sl-input-focus-ring-*` tokens instead.
|
||||
|
||||
| Token | Value |
|
||||
| ------------------------ | ------------------------------------------------------------------------------------- |
|
||||
| `--sl-focus-ring-color` | var(--sl-color-primary-600) (light theme)<br>var(--sl-color-primary-700) (dark theme) |
|
||||
| `--sl-focus-ring-style` | solid |
|
||||
| `--sl-focus-ring-width` | 3px |
|
||||
| `--sl-focus-ring` | var(--sl-focus-ring-style) var(--sl-focus-ring-width) var(--sl-focus-ring-color) |
|
||||
| `--sl-focus-ring-offset` | 1px |
|
||||
|
||||
## Buttons
|
||||
|
||||
Button tokens control the appearance of buttons. In addition, buttons also currently use some form input tokens such as `--sl-input-height-*` and `--sl-input-border-*`. More button tokens may be added in the future to make it easier to style them more independently.
|
||||
|
||||
| Token | Value |
|
||||
| ------------------------------ | --------------------------- |
|
||||
| `--sl-button-font-size-small` | var(--sl-font-size-x-small) |
|
||||
| `--sl-button-font-size-medium` | var(--sl-font-size-small) |
|
||||
| `--sl-button-font-size-large` | var(--sl-font-size-medium) |
|
||||
|
||||
## Form Inputs
|
||||
|
||||
Form input tokens control the appearance of form controls such as [input](/components/input), [select](/components/select), [textarea](/components/textarea), etc.
|
||||
|
||||
| Token | Value |
|
||||
| --------------------------------------- | -------------------------------- |
|
||||
| `--sl-input-height-small` | 1.875rem; (30px @ 16px base) |
|
||||
| `--sl-input-height-medium` | 2.5rem; (40px @ 16px base) |
|
||||
| `--sl-input-height-large` | 3.125rem; (50px @ 16px base) |
|
||||
| `--sl-input-background-color` | var(--sl-color-neutral-0) |
|
||||
| `--sl-input-background-color-hover` | var(--sl-input-background-color) |
|
||||
| `--sl-input-background-color-focus` | var(--sl-input-background-color) |
|
||||
| `--sl-input-background-color-disabled` | var(--sl-color-neutral-100) |
|
||||
| `--sl-input-border-color` | var(--sl-color-neutral-300) |
|
||||
| `--sl-input-border-color-hover` | var(--sl-color-neutral-400) |
|
||||
| `--sl-input-border-color-focus` | var(--sl-color-primary-500) |
|
||||
| `--sl-input-border-color-disabled` | var(--sl-color-neutral-300) |
|
||||
| `--sl-input-border-width` | 1px |
|
||||
| `--sl-input-required-content` | "\*" |
|
||||
| `--sl-input-required-content-offset` | -2px |
|
||||
| `--sl-input-required-content-color` | var(--sl-input-label-color) |
|
||||
| `--sl-input-border-radius-small` | var(--sl-border-radius-medium) |
|
||||
| `--sl-input-border-radius-medium` | var(--sl-border-radius-medium) |
|
||||
| `--sl-input-border-radius-large` | var(--sl-border-radius-medium) |
|
||||
| `--sl-input-font-family` | var(--sl-font-sans) |
|
||||
| `--sl-input-font-weight` | var(--sl-font-weight-normal) |
|
||||
| `--sl-input-font-size-small` | var(--sl-font-size-small) |
|
||||
| `--sl-input-font-size-medium` | var(--sl-font-size-medium) |
|
||||
| `--sl-input-font-size-large` | var(--sl-font-size-large) |
|
||||
| `--sl-input-letter-spacing` | var(--sl-letter-spacing-normal) |
|
||||
| `--sl-input-color` | var(--sl-color-neutral-700) |
|
||||
| `--sl-input-color-hover` | var(--sl-color-neutral-700) |
|
||||
| `--sl-input-color-focus` | var(--sl-color-neutral-700) |
|
||||
| `--sl-input-color-disabled` | var(--sl-color-neutral-900) |
|
||||
| `--sl-input-icon-color` | var(--sl-color-neutral-500) |
|
||||
| `--sl-input-icon-color-hover` | var(--sl-color-neutral-600) |
|
||||
| `--sl-input-icon-color-focus` | var(--sl-color-neutral-600) |
|
||||
| `--sl-input-placeholder-color` | var(--sl-color-neutral-500) |
|
||||
| `--sl-input-placeholder-color-disabled` | var(--sl-color-neutral-600) |
|
||||
| `--sl-input-spacing-small` | var(--sl-spacing-small) |
|
||||
| `--sl-input-spacing-medium` | var(--sl-spacing-medium) |
|
||||
| `--sl-input-spacing-large` | var(--sl-spacing-large) |
|
||||
| `--sl-input-focus-ring-color` | hsl(198.6 88.7% 48.4% / 40%) |
|
||||
| `--sl-input-focus-ring-offset` | 0 |
|
||||
|
||||
## Filled Form Inputs
|
||||
|
||||
Filled form input tokens control the appearance of form controls using the `filled` variant.
|
||||
|
||||
| Token | Value |
|
||||
| --------------------------------------------- | --------------------------- |
|
||||
| `--sl-input-filled-background-color` | var(--sl-color-neutral-100) |
|
||||
| `--sl-input-filled-background-color-hover` | var(--sl-color-neutral-100) |
|
||||
| `--sl-input-filled-background-color-focus` | var(--sl-color-neutral-100) |
|
||||
| `--sl-input-filled-background-color-disabled` | var(--sl-color-neutral-100) |
|
||||
| `--sl-input-filled-color` | var(--sl-color-neutral-800) |
|
||||
| `--sl-input-filled-color-hover` | var(--sl-color-neutral-800) |
|
||||
| `--sl-input-filled-color-focus` | var(--sl-color-neutral-700) |
|
||||
| `--sl-input-filled-color-disabled` | var(--sl-color-neutral-800) |
|
||||
|
||||
## Form Labels
|
||||
|
||||
Form label tokens control the appearance of labels in form controls.
|
||||
|
||||
| Token | Value |
|
||||
| ----------------------------------- | -------------------------- |
|
||||
| `--sl-input-label-font-size-small` | var(--sl-font-size-small) |
|
||||
| `--sl-input-label-font-size-medium` | var(--sl-font-size-medium) |
|
||||
| `--sl-input-label-font-size-large` | var(--sl-font-size-large) |
|
||||
| `--sl-input-label-color` | inherit |
|
||||
|
||||
## Help Text
|
||||
|
||||
Help text tokens control the appearance of help text in form controls.
|
||||
|
||||
| Token | Value |
|
||||
| --------------------------------------- | --------------------------- |
|
||||
| `--sl-input-help-text-font-size-small` | var(--sl-font-size-x-small) |
|
||||
| `--sl-input-help-text-font-size-medium` | var(--sl-font-size-small) |
|
||||
| `--sl-input-help-text-font-size-large` | var(--sl-font-size-medium) |
|
||||
| `--sl-input-help-text-color` | var(--sl-color-neutral-500) |
|
||||
|
||||
## Toggles
|
||||
|
||||
Toggle tokens control the appearance of toggles such as [checkbox](/components/checkbox), [radio](/components/radio), [switch](/components/switch), etc.
|
||||
|
||||
| Token | Value |
|
||||
| ------------------------- | --------------------------- |
|
||||
| `--sl-toggle-size-small` | 0.875rem (14px @ 16px base) |
|
||||
| `--sl-toggle-size-medium` | 1.125rem (18px @ 16px base) |
|
||||
| `--sl-toggle-size-large` | 1.375rem (22px @ 16px base) |
|
||||
|
||||
## Overlays
|
||||
|
||||
Overlay tokens control the appearance of overlays as used in [dialog](/components/dialog), [drawer](/components/drawer), etc.
|
||||
|
||||
| Token | Value |
|
||||
| ------------------------------- | ------------------------- |
|
||||
| `--sl-overlay-background-color` | hsl(240 3.8% 46.1% / 33%) |
|
||||
|
||||
## Panels
|
||||
|
||||
Panel tokens control the appearance of panels such as those used in [dialog](/components/dialog), [drawer](/components/drawer), [menu](/components/menu), etc.
|
||||
|
||||
| Token | Value |
|
||||
| ----------------------------- | --------------------------- |
|
||||
| `--sl-panel-background-color` | var(--sl-color-neutral-0) |
|
||||
| `--sl-panel-border-color` | var(--sl-color-neutral-200) |
|
||||
| `--sl-panel-border-width` | 1px |
|
||||
|
||||
## Tooltips
|
||||
|
||||
Tooltip tokens control the appearance of tooltips. This includes the [tooltip](/components/tooltip) component as well as other implementations, such [range tooltips](/components/range).
|
||||
|
||||
| Token | Value |
|
||||
| ------------------------------- | ---------------------------------------------------- |
|
||||
| `--sl-tooltip-border-radius` | var(--sl-border-radius-medium) |
|
||||
| `--sl-tooltip-background-color` | var(--sl-color-neutral-800) |
|
||||
| `--sl-tooltip-color` | var(--sl-color-neutral-0) |
|
||||
| `--sl-tooltip-font-family` | var(--sl-font-sans) |
|
||||
| `--sl-tooltip-font-weight` | var(--sl-font-weight-normal) |
|
||||
| `--sl-tooltip-font-size` | var(--sl-font-size-small) |
|
||||
| `--sl-tooltip-line-height` | var(--sl-line-height-dense) |
|
||||
| `--sl-tooltip-padding` | var(--sl-spacing-2x-small) var(--sl-spacing-x-small) |
|
||||
| `--sl-tooltip-arrow-size` | 6px |
|
||||
@@ -10,7 +10,7 @@ The default font stack is designed to be simple and highly available on as many
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| `--sl-font-sans` | -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol' | <span style="font-family: var(--sl-font-sans)">The quick brown fox jumped over the lazy dog.</span> |
|
||||
| `--sl-font-serif` | Georgia, 'Times New Roman', serif | <span style="font-family: var(--sl-font-serif)">The quick brown fox jumped over the lazy dog.</span> |
|
||||
| `--sl-font-mono` | Menlo, Monaco, 'Courier New', monospace | <span style="font-family: var(--sl-font-mono)">The quick brown fox jumped over the lazy dog.</span> |
|
||||
| `--sl-font-mono` | SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; | <span style="font-family: var(--sl-font-mono)">The quick brown fox jumped over the lazy dog.</span> |
|
||||
|
||||
## Font Size
|
||||
|
||||
@@ -41,18 +41,18 @@ Font sizes use `rem` units so they scale with the base font size. The pixel valu
|
||||
|
||||
| Token | Value | Example |
|
||||
| ---------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--sl-letter-spacing-denser` | ? | <span style="letter-spacing: var(--sl-letter-spacing-denser);">The quick brown fox jumped over the lazy dog.</span> |
|
||||
| `--sl-letter-spacing-denser` | -0.03em | <span style="letter-spacing: var(--sl-letter-spacing-denser);">The quick brown fox jumped over the lazy dog.</span> |
|
||||
| `--sl-letter-spacing-dense` | -0.015em | <span style="letter-spacing: var(--sl-letter-spacing-dense);">The quick brown fox jumped over the lazy dog.</span> |
|
||||
| `--sl-letter-spacing-normal` | normal | <span style="letter-spacing: var(--sl-letter-spacing-normal);">The quick brown fox jumped over the lazy dog.</span> |
|
||||
| `--sl-letter-spacing-loose` | 0.075em | <span style="letter-spacing: var(--sl-letter-spacing-loose);">The quick brown fox jumped over the lazy dog.</span> |
|
||||
| `--sl-letter-spacing-looser` | ? | <span style="letter-spacing: var(--sl-letter-spacing-looser);">The quick brown fox jumped over the lazy dog.</span> |
|
||||
| `--sl-letter-spacing-looser` | 0.15em | <span style="letter-spacing: var(--sl-letter-spacing-looser);">The quick brown fox jumped over the lazy dog.</span> |
|
||||
|
||||
## Line Height
|
||||
|
||||
| Token | Value | Example |
|
||||
| ------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--sl-line-height-denser` | ? | <div style="line-height: var(--sl-line-height-denser);">The quick brown fox jumped over the lazy dog.<br>The quick brown fox jumped over the lazy dog.<br>The quick brown fox jumped over the lazy dog.</div> |
|
||||
| `--sl-line-height-denser` | 1 | <div style="line-height: var(--sl-line-height-denser);">The quick brown fox jumped over the lazy dog.<br>The quick brown fox jumped over the lazy dog.<br>The quick brown fox jumped over the lazy dog.</div> |
|
||||
| `--sl-line-height-dense` | 1.4 | <div style="line-height: var(--sl-line-height-dense);">The quick brown fox jumped over the lazy dog.<br>The quick brown fox jumped over the lazy dog.<br>The quick brown fox jumped over the lazy dog.</div> |
|
||||
| `--sl-line-height-normal` | 1.8 | <div style="line-height: var(--sl-line-height-normal);">The quick brown fox jumped over the lazy dog.<br>The quick brown fox jumped over the lazy dog.<br>The quick brown fox jumped over the lazy dog.</div> |
|
||||
| `--sl-line-height-loose` | 2.2 | <div style="line-height: var(--sl-line-height-loose);">The quick brown fox jumped over the lazy dog.<br>The quick brown fox jumped over the lazy dog.<br>The quick brown fox jumped over the lazy dog.</div> |
|
||||
| `--sl-line-height-looser` | ? | <div style="line-height: var(--sl-line-height-looser);">The quick brown fox jumped over the lazy dog.<br>The quick brown fox jumped over the lazy dog.<br>The quick brown fox jumped over the lazy dog.</div> |
|
||||
| `--sl-line-height-looser` | 2.6 | <div style="line-height: var(--sl-line-height-looser);">The quick brown fox jumped over the lazy dog.<br>The quick brown fox jumped over the lazy dog.<br>The quick brown fox jumped over the lazy dog.</div> |
|
||||
|
||||
4976
package-lock.json
generated
4976
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
71
package.json
71
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@shoelace-style/shoelace",
|
||||
"description": "A forward-thinking library of web components.",
|
||||
"version": "2.0.0-beta.84",
|
||||
"version": "2.0.0",
|
||||
"homepage": "https://github.com/shoelace-style/shoelace",
|
||||
"author": "Cory LaViska",
|
||||
"license": "MIT",
|
||||
@@ -17,6 +17,7 @@
|
||||
"./dist/themes/*": "./dist/themes/*",
|
||||
"./dist/components/*": "./dist/components/*",
|
||||
"./dist/utilities/*": "./dist/utilities/*",
|
||||
"./dist/react": "./dist/react/index.js",
|
||||
"./dist/react/*": "./dist/react/*",
|
||||
"./dist/translations/*": "./dist/translations/*"
|
||||
},
|
||||
@@ -50,72 +51,74 @@
|
||||
"lint:fix": "eslint src --max-warnings 0 --fix",
|
||||
"ts-check": "tsc --noEmit --project ./tsconfig.json",
|
||||
"create": "plop --plopfile scripts/plop/plopfile.js",
|
||||
"test": "web-test-runner",
|
||||
"test:component": "npm run test -- --watch --group",
|
||||
"test:watch": "web-test-runner --watch",
|
||||
"test": "web-test-runner --group default",
|
||||
"test:component": "web-test-runner -- --watch --group",
|
||||
"test:watch": "web-test-runner --watch --group default",
|
||||
"spellcheck": "cspell \"**/*.{js,ts,json,html,css,md}\" --no-progress",
|
||||
"list-outdated-dependencies": "npm-check-updates --format repo --peer",
|
||||
"update-dependencies": "npm-check-updates --peer -u && npm install && npm run lint:fix && npm run prettier && npm run verify"
|
||||
"update-dependencies": "npm-check-updates --peer -u && npm install"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.0.6",
|
||||
"@lit-labs/react": "^1.1.0",
|
||||
"@ctrl/tinycolor": "^3.5.0",
|
||||
"@floating-ui/dom": "^1.1.0",
|
||||
"@lit-labs/react": "^1.1.1",
|
||||
"@shoelace-style/animations": "^1.1.0",
|
||||
"@shoelace-style/localize": "^3.0.3",
|
||||
"color": "4.2",
|
||||
"lit": "^2.4.1",
|
||||
"@shoelace-style/localize": "^3.0.4",
|
||||
"lit": "^2.6.1",
|
||||
"qr-creator": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@custom-elements-manifest/analyzer": "^0.6.6",
|
||||
"@custom-elements-manifest/analyzer": "^0.6.8",
|
||||
"@open-wc/testing": "^3.1.7",
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/mocha": "^10.0.0",
|
||||
"@types/react": "^18.0.25",
|
||||
"@typescript-eslint/eslint-plugin": "^5.43.0",
|
||||
"@typescript-eslint/parser": "^5.43.0",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@types/react": "^18.0.26",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.1",
|
||||
"@typescript-eslint/parser": "^5.48.1",
|
||||
"@web/dev-server-esbuild": "^0.3.3",
|
||||
"@web/test-runner": "^0.15.0",
|
||||
"@web/test-runner-commands": "^0.6.5",
|
||||
"@web/test-runner-playwright": "^0.9.0",
|
||||
"bootstrap-icons": "^1.10.2",
|
||||
"browser-sync": "^2.27.10",
|
||||
"chalk": "^5.1.2",
|
||||
"bootstrap-icons": "^1.10.3",
|
||||
"browser-sync": "^2.27.11",
|
||||
"cem-plugin-vs-code-custom-data-generator": "^1.3.2",
|
||||
"chalk": "^5.2.0",
|
||||
"command-line-args": "^5.2.1",
|
||||
"comment-parser": "^1.3.1",
|
||||
"cspell": "^6.14.2",
|
||||
"cspell": "^6.18.1",
|
||||
"del": "^7.0.0",
|
||||
"download": "^8.0.0",
|
||||
"esbuild": "^0.15.14",
|
||||
"eslint": "^8.27.0",
|
||||
"esbuild": "^0.16.17",
|
||||
"eslint": "^8.31.0",
|
||||
"eslint-plugin-chai-expect": "^3.0.0",
|
||||
"eslint-plugin-chai-friendly": "^0.7.2",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-lit": "^1.6.1",
|
||||
"eslint-plugin-lit-a11y": "^2.2.3",
|
||||
"eslint-plugin-import": "^2.27.4",
|
||||
"eslint-plugin-lit": "^1.8.2",
|
||||
"eslint-plugin-lit-a11y": "^2.3.0",
|
||||
"eslint-plugin-markdown": "^3.0.0",
|
||||
"eslint-plugin-wc": "^1.3.2",
|
||||
"eslint-plugin-sort-imports-es6-autofix": "^0.6.0",
|
||||
"eslint-plugin-wc": "^1.4.0",
|
||||
"front-matter": "^4.0.2",
|
||||
"get-port": "^6.1.2",
|
||||
"globby": "^13.1.2",
|
||||
"husky": "^8.0.2",
|
||||
"jsonata": "^1.8.6",
|
||||
"lint-staged": "^13.0.3",
|
||||
"globby": "^13.1.3",
|
||||
"husky": "^8.0.3",
|
||||
"jsonata": "^2.0.1",
|
||||
"lint-staged": "^13.1.0",
|
||||
"lunr": "^2.3.9",
|
||||
"npm-check-updates": "^16.4.1",
|
||||
"npm-check-updates": "^16.6.2",
|
||||
"open": "^8.4.0",
|
||||
"pascal-case": "^3.1.2",
|
||||
"plop": "^3.1.1",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier": "^2.8.2",
|
||||
"react": "^18.2.0",
|
||||
"recursive-copy": "^2.0.14",
|
||||
"sinon": "^14.0.2",
|
||||
"sinon": "^15.0.1",
|
||||
"source-map": "^0.7.4",
|
||||
"strip-css-comments": "^5.0.0",
|
||||
"tslib": "^2.4.1",
|
||||
"typescript": "4.8.4",
|
||||
"typescript": "4.9.4",
|
||||
"user-agent-data-types": "^0.3.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
||||
@@ -28,7 +28,6 @@ fs.mkdirSync(outdir, { recursive: true });
|
||||
execSync(`node scripts/make-metadata.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-search.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-react.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-vscode-data.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-web-types.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-themes.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-icons.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
@@ -121,15 +120,6 @@ fs.mkdirSync(outdir, { recursive: true });
|
||||
routes: {
|
||||
'/dist': './dist'
|
||||
}
|
||||
},
|
||||
socket: {
|
||||
socketIoClientConfig: {
|
||||
// Configure socketIO to retry forever when disconnected to enable the auto-reattach timeout below to work
|
||||
reconnectionAttempts: Infinity,
|
||||
reconnectionDelay: 500,
|
||||
reconnectionDelayMax: 500,
|
||||
timeout: 1000
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import fs from 'fs';
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import { deleteSync } from 'del';
|
||||
import { pascalCase } from 'pascal-case';
|
||||
import prettier from 'prettier';
|
||||
import prettierConfig from '../prettier.config.cjs';
|
||||
import { getAllComponents } from './shared.js';
|
||||
@@ -40,14 +39,14 @@ components.map(component => {
|
||||
import { createComponent } from '@lit-labs/react';
|
||||
import Component from '../../${importPath}';
|
||||
|
||||
export default createComponent(
|
||||
React,
|
||||
'${component.tagName}',
|
||||
Component,
|
||||
{
|
||||
export default createComponent({
|
||||
tagName: '${component.tagName}',
|
||||
elementClass: Component,
|
||||
react: React,
|
||||
events: {
|
||||
${events}
|
||||
}
|
||||
);
|
||||
});
|
||||
`,
|
||||
Object.assign(prettierConfig, {
|
||||
parser: 'babel-ts'
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
//
|
||||
// This script generates vscode.html-custom-data.json (for IntelliSense).
|
||||
//
|
||||
// You must generate dist/custom-elements.json before running this script.
|
||||
//
|
||||
import commandLineArgs from 'command-line-args';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { getAllComponents } from './shared.js';
|
||||
|
||||
const { outdir } = commandLineArgs({ name: 'outdir', type: String });
|
||||
const metadata = JSON.parse(fs.readFileSync(path.join(outdir, 'custom-elements.json'), 'utf8'));
|
||||
|
||||
console.log('Generating custom data for VS Code');
|
||||
|
||||
const components = getAllComponents(metadata);
|
||||
const vscode = { tags: [] };
|
||||
|
||||
components.map(component => {
|
||||
const name = component.tagName;
|
||||
const attributes = component.attributes?.map(attr => {
|
||||
const type = attr.type?.text;
|
||||
let values = [];
|
||||
|
||||
if (type) {
|
||||
type.split('|').map(val => {
|
||||
val = val.trim();
|
||||
|
||||
// Only accept values that are strings and numbers
|
||||
const isString = val.startsWith(`'`);
|
||||
const isNumber = Number(val).toString() === val;
|
||||
|
||||
if (isString) {
|
||||
// Remove quotes
|
||||
val = val.replace(/^'/, '').replace(/'$/, '');
|
||||
}
|
||||
|
||||
if (isNumber || isString) {
|
||||
values.push({ name: val });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (values.length === 0) {
|
||||
values = undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
name: attr.name,
|
||||
description: attr.description,
|
||||
values
|
||||
};
|
||||
});
|
||||
|
||||
vscode.tags.push({ name, attributes });
|
||||
});
|
||||
|
||||
fs.writeFileSync(path.join(outdir, 'vscode.html-custom-data.json'), JSON.stringify(vscode, null, 2), 'utf8');
|
||||
@@ -8,9 +8,9 @@ import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
* @summary Short summary of the component's intended use.
|
||||
*
|
||||
* @since 2.0
|
||||
* @documentation https://shoelace.style/components/{{ tagWithoutPrefix tag }}
|
||||
* @status experimental
|
||||
* @since 2.0
|
||||
*
|
||||
* @dependency sl-example
|
||||
*
|
||||
@@ -19,7 +19,7 @@ import type { CSSResultGroup } from 'lit';
|
||||
* @slot - The default slot.
|
||||
* @slot example - An example slot.
|
||||
*
|
||||
* @csspart base - The component's internal wrapper.
|
||||
* @csspart base - The component's base wrapper.
|
||||
*
|
||||
* @cssproperty --example - An example CSS custom property.
|
||||
*/
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** Gets an array of components from a CEM object. */
|
||||
export function getAllComponents(metadata) {
|
||||
const allComponents = [];
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ export default css`
|
||||
border: solid var(--sl-panel-border-width) var(--sl-panel-border-color);
|
||||
border-top-width: calc(var(--sl-panel-border-width) * 3);
|
||||
border-radius: var(--sl-border-radius-medium);
|
||||
box-shadow: var(--box-shadow);
|
||||
font-family: var(--sl-font-sans);
|
||||
font-size: var(--sl-font-size-small);
|
||||
font-weight: var(--sl-font-weight-normal);
|
||||
@@ -83,6 +82,7 @@ export default css`
|
||||
|
||||
.alert__message {
|
||||
flex: 1 1 auto;
|
||||
display: block;
|
||||
padding: var(--sl-spacing-large);
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -91,7 +91,7 @@ export default css`
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--sl-font-size-large);
|
||||
font-size: var(--sl-font-size-medium);
|
||||
padding-inline-end: var(--sl-spacing-medium);
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import '../icon-button/icon-button';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { html } from 'lit';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import styles from './alert.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
@@ -16,27 +16,25 @@ const toastStack = Object.assign(document.createElement('div'), { className: 'sl
|
||||
|
||||
/**
|
||||
* @summary Alerts are used to display important messages inline or as toast notifications.
|
||||
*
|
||||
* @since 2.0
|
||||
* @documentation https://shoelace.style/components/alert
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @dependency sl-icon-button
|
||||
*
|
||||
* @slot - The alert's content.
|
||||
* @slot icon - An icon to show in the alert.
|
||||
* @slot - The alert's main content.
|
||||
* @slot icon - An icon to show in the alert. Works best with `<sl-icon>`.
|
||||
*
|
||||
* @event sl-show - Emitted when the alert opens.
|
||||
* @event sl-after-show - Emitted after the alert opens and all animations are complete.
|
||||
* @event sl-hide - Emitted when the alert closes.
|
||||
* @event sl-after-hide - Emitted after the alert closes and all animations are complete.
|
||||
*
|
||||
* @csspart base - The component's internal wrapper.
|
||||
* @csspart icon - The container that wraps the alert icon.
|
||||
* @csspart message - The alert message.
|
||||
* @csspart close-button - The close button.
|
||||
* @csspart close-button__base - The close button's `base` part.
|
||||
*
|
||||
* @cssproperty --box-shadow - The alert's box shadow.
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart icon - The container that wraps the optional icon.
|
||||
* @csspart message - The container that wraps the alert's main content.
|
||||
* @csspart close-button - The close button, an `<sl-icon-button>`.
|
||||
* @csspart close-button__base - The close button's exported `base` part.
|
||||
*
|
||||
* @animation alert.show - The animation to use when showing the alert.
|
||||
* @animation alert.hide - The animation to use when hiding the alert.
|
||||
@@ -52,18 +50,22 @@ export default class SlAlert extends ShoelaceElement {
|
||||
|
||||
@query('[part~="base"]') base: HTMLElement;
|
||||
|
||||
/** Indicates whether or not the alert is open. You can use this in lieu of the show/hide methods. */
|
||||
/**
|
||||
* Indicates whether or not the alert is open. You can toggle this attribute to show and hide the alert, or you can
|
||||
* use the `show()` and `hide()` methods and this attribute will reflect the alert's open state.
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true }) open = false;
|
||||
|
||||
/** Makes the alert closable. */
|
||||
/** Enables a close button that allows the user to dismiss the alert. */
|
||||
@property({ type: Boolean, reflect: true }) closable = false;
|
||||
|
||||
/** The alert's variant. */
|
||||
/** The alert's theme variant. */
|
||||
@property({ reflect: true }) variant: 'primary' | 'success' | 'neutral' | 'warning' | 'danger' = 'primary';
|
||||
|
||||
/**
|
||||
* The length of time, in milliseconds, the alert will show before closing itself. If the user interacts with
|
||||
* the alert before it closes (e.g. moves the mouse over it), the timer will restart. Defaults to `Infinity`.
|
||||
* the alert before it closes (e.g. moves the mouse over it), the timer will restart. Defaults to `Infinity`, meaning
|
||||
* the alert will not close on its own.
|
||||
*/
|
||||
@property({ type: Number }) duration = Infinity;
|
||||
|
||||
@@ -71,6 +73,57 @@ export default class SlAlert extends ShoelaceElement {
|
||||
this.base.hidden = !this.open;
|
||||
}
|
||||
|
||||
private restartAutoHide() {
|
||||
clearTimeout(this.autoHideTimeout);
|
||||
if (this.open && this.duration < Infinity) {
|
||||
this.autoHideTimeout = window.setTimeout(() => this.hide(), this.duration);
|
||||
}
|
||||
}
|
||||
|
||||
private handleCloseClick() {
|
||||
this.hide();
|
||||
}
|
||||
|
||||
private handleMouseMove() {
|
||||
this.restartAutoHide();
|
||||
}
|
||||
|
||||
@watch('open', { waitUntilFirstUpdate: true })
|
||||
async handleOpenChange() {
|
||||
if (this.open) {
|
||||
// Show
|
||||
this.emit('sl-show');
|
||||
|
||||
if (this.duration < Infinity) {
|
||||
this.restartAutoHide();
|
||||
}
|
||||
|
||||
await stopAnimations(this.base);
|
||||
this.base.hidden = false;
|
||||
const { keyframes, options } = getAnimation(this, 'alert.show', { dir: this.localize.dir() });
|
||||
await animateTo(this.base, keyframes, options);
|
||||
|
||||
this.emit('sl-after-show');
|
||||
} else {
|
||||
// Hide
|
||||
this.emit('sl-hide');
|
||||
|
||||
clearTimeout(this.autoHideTimeout);
|
||||
|
||||
await stopAnimations(this.base);
|
||||
const { keyframes, options } = getAnimation(this, 'alert.hide', { dir: this.localize.dir() });
|
||||
await animateTo(this.base, keyframes, options);
|
||||
this.base.hidden = true;
|
||||
|
||||
this.emit('sl-after-hide');
|
||||
}
|
||||
}
|
||||
|
||||
@watch('duration')
|
||||
handleDurationChange() {
|
||||
this.restartAutoHide();
|
||||
}
|
||||
|
||||
/** Shows the alert. */
|
||||
async show() {
|
||||
if (this.open) {
|
||||
@@ -127,57 +180,6 @@ export default class SlAlert extends ShoelaceElement {
|
||||
});
|
||||
}
|
||||
|
||||
restartAutoHide() {
|
||||
clearTimeout(this.autoHideTimeout);
|
||||
if (this.open && this.duration < Infinity) {
|
||||
this.autoHideTimeout = window.setTimeout(() => this.hide(), this.duration);
|
||||
}
|
||||
}
|
||||
|
||||
handleCloseClick() {
|
||||
this.hide();
|
||||
}
|
||||
|
||||
handleMouseMove() {
|
||||
this.restartAutoHide();
|
||||
}
|
||||
|
||||
@watch('open', { waitUntilFirstUpdate: true })
|
||||
async handleOpenChange() {
|
||||
if (this.open) {
|
||||
// Show
|
||||
this.emit('sl-show');
|
||||
|
||||
if (this.duration < Infinity) {
|
||||
this.restartAutoHide();
|
||||
}
|
||||
|
||||
await stopAnimations(this.base);
|
||||
this.base.hidden = false;
|
||||
const { keyframes, options } = getAnimation(this, 'alert.show', { dir: this.localize.dir() });
|
||||
await animateTo(this.base, keyframes, options);
|
||||
|
||||
this.emit('sl-after-show');
|
||||
} else {
|
||||
// Hide
|
||||
this.emit('sl-hide');
|
||||
|
||||
clearTimeout(this.autoHideTimeout);
|
||||
|
||||
await stopAnimations(this.base);
|
||||
const { keyframes, options } = getAnimation(this, 'alert.hide', { dir: this.localize.dir() });
|
||||
await animateTo(this.base, keyframes, options);
|
||||
this.base.hidden = true;
|
||||
|
||||
this.emit('sl-after-hide');
|
||||
}
|
||||
}
|
||||
|
||||
@watch('duration')
|
||||
handleDurationChange() {
|
||||
this.restartAutoHide();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div
|
||||
@@ -194,18 +196,12 @@ export default class SlAlert extends ShoelaceElement {
|
||||
'alert--danger': this.variant === 'danger'
|
||||
})}
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
aria-atomic="true"
|
||||
aria-hidden=${this.open ? 'false' : 'true'}
|
||||
@mousemove=${this.handleMouseMove}
|
||||
>
|
||||
<span part="icon" class="alert__icon">
|
||||
<slot name="icon"></slot>
|
||||
</span>
|
||||
<slot name="icon" part="icon" class="alert__icon"></slot>
|
||||
|
||||
<span part="message" class="alert__message">
|
||||
<slot></slot>
|
||||
</span>
|
||||
<slot part="message" class="alert__message" aria-live="polite"></slot>
|
||||
|
||||
${this.closable
|
||||
? html`
|
||||
@@ -213,8 +209,9 @@ export default class SlAlert extends ShoelaceElement {
|
||||
part="close-button"
|
||||
exportparts="base:close-button__base"
|
||||
class="alert__close-button"
|
||||
name="x"
|
||||
name="x-lg"
|
||||
library="system"
|
||||
label=${this.localize.term('close')}
|
||||
@click=${this.handleCloseClick}
|
||||
></sl-icon-button>
|
||||
`
|
||||
@@ -226,16 +223,16 @@ export default class SlAlert extends ShoelaceElement {
|
||||
|
||||
setDefaultAnimation('alert.show', {
|
||||
keyframes: [
|
||||
{ opacity: 0, transform: 'scale(0.8)' },
|
||||
{ opacity: 1, transform: 'scale(1)' }
|
||||
{ opacity: 0, scale: 0.8 },
|
||||
{ opacity: 1, scale: 1 }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
setDefaultAnimation('alert.hide', {
|
||||
keyframes: [
|
||||
{ opacity: 1, transform: 'scale(1)' },
|
||||
{ opacity: 0, transform: 'scale(0.8)' }
|
||||
{ opacity: 1, scale: 1 },
|
||||
{ opacity: 0, scale: 0.8 }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ export default css`
|
||||
:host {
|
||||
--control-box-size: 3rem;
|
||||
--icon-size: calc(var(--control-box-size) * 0.625);
|
||||
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
@@ -43,10 +44,14 @@ export default css`
|
||||
|
||||
:host([play]:hover) .animated-image__control-box {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
:host([play]:not(:hover)) .animated-image__control-box {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
:host([play]) slot[name='pause-icon'],
|
||||
:host(:not([play])) slot[name='play-icon'] {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { watch } from '../../internal/watch';
|
||||
import '../icon/icon';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { html } from 'lit';
|
||||
import { watch } from '../../internal/watch';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import styles from './animated-image.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
* @summary A component for displaying animated GIFs and WEBPs that play and pause on interaction.
|
||||
*
|
||||
* @since 2.0
|
||||
* @documentation https://shoelace.style/components/animated-image
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @dependency sl-icon
|
||||
*
|
||||
* @event sl-load - Emitted when the image loads successfully.
|
||||
* @event sl-error - Emitted when the image fails to load.
|
||||
*
|
||||
* @slot play-icon - Optional play icon to use instead of the default. Works best with `<sl-icon>`.
|
||||
* @slot pause-icon - Optional pause icon to use instead of the default. Works best with `<sl-icon>`.
|
||||
*
|
||||
* @part - control-box - The container that surrounds the pause/play icons and provides their background.
|
||||
* @part - play-icon - The icon to use for the play button.
|
||||
* @part - pause-icon - The icon to use for the pause button.
|
||||
*
|
||||
* @cssproperty --control-box-size - The size of the icon box.
|
||||
* @cssproperty --icon-size - The size of the play/pause icons.
|
||||
@@ -28,25 +29,25 @@ import type { CSSResultGroup } from 'lit';
|
||||
export default class SlAnimatedImage extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
@query('.animated-image__animated') animatedImage: HTMLImageElement;
|
||||
|
||||
@state() frozenFrame: string;
|
||||
@state() isLoaded = false;
|
||||
|
||||
@query('.animated-image__animated') animatedImage: HTMLImageElement;
|
||||
|
||||
/** The image's src attribute. */
|
||||
/** The path to the image to load. */
|
||||
@property() src: string;
|
||||
|
||||
/** The image's alt attribute. */
|
||||
/** A description of the image used by assistive devices. */
|
||||
@property() alt: string;
|
||||
|
||||
/** When set, the image will animate. Otherwise, it will be paused. */
|
||||
/** Plays the animation. When this attribute is remove, the animation will pause. */
|
||||
@property({ type: Boolean, reflect: true }) play: boolean;
|
||||
|
||||
handleClick() {
|
||||
private handleClick() {
|
||||
this.play = !this.play;
|
||||
}
|
||||
|
||||
handleLoad() {
|
||||
private handleLoad() {
|
||||
const canvas = document.createElement('canvas');
|
||||
const { width, height } = this.animatedImage;
|
||||
canvas.width = width;
|
||||
@@ -60,7 +61,7 @@ export default class SlAnimatedImage extends ShoelaceElement {
|
||||
}
|
||||
}
|
||||
|
||||
handleError() {
|
||||
private handleError() {
|
||||
this.emit('sl-error');
|
||||
}
|
||||
|
||||
@@ -104,9 +105,8 @@ export default class SlAnimatedImage extends ShoelaceElement {
|
||||
/>
|
||||
|
||||
<div part="control-box" class="animated-image__control-box">
|
||||
${this.play
|
||||
? html`<sl-icon part="pause-icon" name="pause-fill" library="system"></sl-icon>`
|
||||
: html`<sl-icon part="play-icon" name="play-fill" library="system"></sl-icon>`}
|
||||
<slot name="play-icon"><sl-icon name="play-fill" library="system"></sl-icon></slot>
|
||||
<slot name="pause-icon"><sl-icon name="pause-fill" library="system"></sl-icon></slot>
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property, queryAsync } from 'lit/decorators.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { watch } from '../../internal/watch';
|
||||
import styles from './animation.styles';
|
||||
import { animations } from './animations';
|
||||
import { customElement, property, queryAsync } from 'lit/decorators.js';
|
||||
import { html } from 'lit';
|
||||
import { watch } from '../../internal/watch';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import styles from './animation.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
* @summary Animate elements declaratively with nearly 100 baked-in presets, or roll your own with custom keyframes. Powered by the [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API).
|
||||
*
|
||||
* @since 2.0
|
||||
* @documentation https://shoelace.style/components/animation
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @event sl-cancel - Emitted when the animation is canceled.
|
||||
* @event sl-finish - Emitted when the animation finishes.
|
||||
* @event sl-start - Emitted when the animation starts or restarts.
|
||||
*
|
||||
* @slot - The element to animate. If multiple elements are to be animated, wrap them in a single container or use
|
||||
* multiple animation elements.
|
||||
* @slot - The element to animate. Avoid slotting in more than one element, as subsequent ones will be ignored. To
|
||||
* animate multiple elements, either wrap them in a single container or use multiple `<sl-animation>` elements.
|
||||
*/
|
||||
@customElement('sl-animation')
|
||||
export default class SlAnimation extends ShoelaceElement {
|
||||
@@ -40,7 +40,10 @@ export default class SlAnimation extends ShoelaceElement {
|
||||
/** The number of milliseconds to delay the start of the animation. */
|
||||
@property({ type: Number }) delay = 0;
|
||||
|
||||
/** Determines the direction of playback as well as the behavior when reaching the end of an iteration. */
|
||||
/**
|
||||
* Determines the direction of playback as well as the behavior when reaching the end of an iteration.
|
||||
* [Learn more](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-direction)
|
||||
*/
|
||||
@property() direction: PlaybackDirection = 'normal';
|
||||
|
||||
/** The number of milliseconds each iteration of the animation takes to complete. */
|
||||
@@ -97,68 +100,24 @@ export default class SlAnimation extends ShoelaceElement {
|
||||
this.destroyAnimation();
|
||||
}
|
||||
|
||||
@watch('name')
|
||||
@watch('delay')
|
||||
@watch('direction')
|
||||
@watch('duration')
|
||||
@watch('easing')
|
||||
@watch('endDelay')
|
||||
@watch('fill')
|
||||
@watch('iterations')
|
||||
@watch('iterationsStart')
|
||||
@watch('keyframes')
|
||||
handleAnimationChange() {
|
||||
if (!this.hasUpdated) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.createAnimation();
|
||||
}
|
||||
|
||||
handleAnimationFinish() {
|
||||
private handleAnimationFinish() {
|
||||
this.play = false;
|
||||
this.hasStarted = false;
|
||||
this.emit('sl-finish');
|
||||
}
|
||||
|
||||
handleAnimationCancel() {
|
||||
private handleAnimationCancel() {
|
||||
this.play = false;
|
||||
this.hasStarted = false;
|
||||
this.emit('sl-cancel');
|
||||
}
|
||||
|
||||
@watch('play')
|
||||
handlePlayChange() {
|
||||
if (this.animation) {
|
||||
if (this.play && !this.hasStarted) {
|
||||
this.hasStarted = true;
|
||||
this.emit('sl-start');
|
||||
}
|
||||
|
||||
if (this.play) {
|
||||
this.animation.play();
|
||||
} else {
|
||||
this.animation.pause();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@watch('playbackRate')
|
||||
handlePlaybackRateChange() {
|
||||
if (this.animation) {
|
||||
this.animation.playbackRate = this.playbackRate;
|
||||
}
|
||||
}
|
||||
|
||||
handleSlotChange() {
|
||||
private handleSlotChange() {
|
||||
this.destroyAnimation();
|
||||
this.createAnimation();
|
||||
}
|
||||
|
||||
async createAnimation() {
|
||||
private async createAnimation() {
|
||||
const easing = animations.easings[this.easing] ?? this.easing;
|
||||
const keyframes = this.keyframes ?? (animations as unknown as Partial<Record<string, Keyframe[]>>)[this.name];
|
||||
const slot = await this.defaultSlot;
|
||||
@@ -193,7 +152,7 @@ export default class SlAnimation extends ShoelaceElement {
|
||||
return true;
|
||||
}
|
||||
|
||||
destroyAnimation() {
|
||||
private destroyAnimation() {
|
||||
if (this.animation) {
|
||||
this.animation.cancel();
|
||||
this.animation.removeEventListener('cancel', this.handleAnimationCancel);
|
||||
@@ -202,7 +161,53 @@ export default class SlAnimation extends ShoelaceElement {
|
||||
}
|
||||
}
|
||||
|
||||
/** Clears all KeyframeEffects caused by this animation and aborts its playback. */
|
||||
@watch([
|
||||
'name',
|
||||
'delay',
|
||||
'direction',
|
||||
'duration',
|
||||
'easing',
|
||||
'endDelay',
|
||||
'fill',
|
||||
'iterations',
|
||||
'iterationsStart',
|
||||
'keyframes'
|
||||
])
|
||||
handleAnimationChange() {
|
||||
if (!this.hasUpdated) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.createAnimation();
|
||||
}
|
||||
|
||||
@watch('play')
|
||||
handlePlayChange() {
|
||||
if (this.animation) {
|
||||
if (this.play && !this.hasStarted) {
|
||||
this.hasStarted = true;
|
||||
this.emit('sl-start');
|
||||
}
|
||||
|
||||
if (this.play) {
|
||||
this.animation.play();
|
||||
} else {
|
||||
this.animation.pause();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@watch('playbackRate')
|
||||
handlePlaybackRateChange() {
|
||||
if (this.animation) {
|
||||
this.animation.playbackRate = this.playbackRate;
|
||||
}
|
||||
}
|
||||
|
||||
/** Clears all keyframe effects caused by this animation and aborts its playback. */
|
||||
cancel() {
|
||||
this.animation?.cancel();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@ import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
import type SlAvatar from './avatar';
|
||||
|
||||
// The default avatar background just misses AA contrast, but the next step up is way too dark. Since avatars aren't
|
||||
// used to display text, we're going to relax this rule.
|
||||
const ignoredRules = ['color-contrast'];
|
||||
|
||||
describe('<sl-avatar>', () => {
|
||||
let el: SlAvatar;
|
||||
|
||||
@@ -11,7 +15,7 @@ describe('<sl-avatar>', () => {
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
await expect(el).to.be.accessible({ ignoredRules });
|
||||
});
|
||||
|
||||
it('should default to circle styling', () => {
|
||||
@@ -36,7 +40,7 @@ describe('<sl-avatar>', () => {
|
||||
* the image element to pass accessibility.
|
||||
* https://html.spec.whatwg.org/multipage/images.html#ancillary-images
|
||||
*/
|
||||
await expect(el).to.be.accessible();
|
||||
await expect(el).to.be.accessible({ ignoredRules });
|
||||
});
|
||||
|
||||
it('renders "image" part, with src and a role of presentation', () => {
|
||||
@@ -59,7 +63,7 @@ describe('<sl-avatar>', () => {
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
await expect(el).to.be.accessible({ ignoredRules });
|
||||
});
|
||||
|
||||
it('renders "initials" part, with initials as the text node', () => {
|
||||
@@ -76,7 +80,7 @@ describe('<sl-avatar>', () => {
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
await expect(el).to.be.accessible({ ignoredRules });
|
||||
});
|
||||
|
||||
it('appends the appropriate class on the "base" part', () => {
|
||||
@@ -94,7 +98,7 @@ describe('<sl-avatar>', () => {
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
await expect(el).to.be.accessible({ ignoredRules });
|
||||
});
|
||||
|
||||
it('should accept as an assigned child in the shadow root', () => {
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { watch } from '../../internal/watch';
|
||||
import '../icon/icon';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { html } from 'lit';
|
||||
import { watch } from '../../internal/watch';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import styles from './avatar.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
* @summary Avatars are used to represent a person or object.
|
||||
*
|
||||
* @since 2.0
|
||||
* @documentation https://shoelace.style/components/avatar
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @dependency sl-icon
|
||||
*
|
||||
* @slot icon - The default icon to use when no image or initials are present.
|
||||
* @slot icon - The default icon to use when no image or initials are present. Works best with `<sl-icon>`.
|
||||
*
|
||||
* @csspart base - The component's internal wrapper.
|
||||
* @csspart icon - The container that wraps the avatar icon.
|
||||
* @csspart initials - The container that wraps the avatar initials.
|
||||
* @csspart image - The avatar image.
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart icon - The container that wraps the avatar's icon.
|
||||
* @csspart initials - The container that wraps the avatar's initials.
|
||||
* @csspart image - The avatar image. Only shown when the `image` attribute is set.
|
||||
*
|
||||
* @cssproperty --size - The size of the avatar.
|
||||
*/
|
||||
@@ -67,11 +67,9 @@ export default class SlAvatar extends ShoelaceElement {
|
||||
${this.initials
|
||||
? html` <div part="initials" class="avatar__initials">${this.initials}</div> `
|
||||
: html`
|
||||
<div part="icon" class="avatar__icon" aria-hidden="true">
|
||||
<slot name="icon">
|
||||
<sl-icon name="person-fill" library="system"></sl-icon>
|
||||
</slot>
|
||||
</div>
|
||||
<slot name="icon" part="icon" class="avatar__icon" aria-hidden="true">
|
||||
<sl-icon name="person-fill" library="system"></sl-icon>
|
||||
</slot>
|
||||
`}
|
||||
${this.image && !this.hasError
|
||||
? html`
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { html } from 'lit';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import styles from './badge.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
* @summary Badges are used to draw attention and display statuses or counts.
|
||||
*
|
||||
* @since 2.0
|
||||
* @documentation https://shoelace.style/components/badge
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @slot - The badge's content.
|
||||
*
|
||||
* @csspart base - The component's internal wrapper.
|
||||
* @csspart base - The component's base wrapper.
|
||||
*/
|
||||
@customElement('sl-badge')
|
||||
export default class SlBadge extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
/** The badge's variant. */
|
||||
/** The badge's theme variant. */
|
||||
@property({ reflect: true }) variant: 'primary' | 'success' | 'neutral' | 'warning' | 'danger' = 'primary';
|
||||
|
||||
/** Draws a pill-style badge with rounded edges. */
|
||||
@@ -30,7 +30,7 @@ export default class SlBadge extends ShoelaceElement {
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<span
|
||||
<slot
|
||||
part="base"
|
||||
class=${classMap({
|
||||
badge: true,
|
||||
@@ -43,9 +43,7 @@ export default class SlBadge extends ShoelaceElement {
|
||||
'badge--pulse': this.pulse
|
||||
})}
|
||||
role="status"
|
||||
>
|
||||
<slot></slot>
|
||||
</span>
|
||||
></slot>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { html } from 'lit';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import styles from './breadcrumb-item.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
* @summary Breadcrumb Items are used inside [breadcrumbs](/components/breadcrumb) to represent different links.
|
||||
*
|
||||
* @since 2.0
|
||||
* @documentation https://shoelace.style/components/breadcrumb-item
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @slot - The breadcrumb item's label.
|
||||
* @slot prefix - An optional prefix, usually an icon or icon button.
|
||||
@@ -19,11 +19,11 @@ import type { CSSResultGroup } from 'lit';
|
||||
* @slot separator - The separator to use for the breadcrumb item. This will only change the separator for this item. If
|
||||
* you want to change it for all items in the group, set the separator on `<sl-breadcrumb>` instead.
|
||||
*
|
||||
* @csspart base - The component's internal wrapper.
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart label - The breadcrumb item's label.
|
||||
* @csspart prefix - The container that wraps the prefix slot.
|
||||
* @csspart suffix - The container that wraps the suffix slot.
|
||||
* @csspart separator - The container that wraps the separator slot.
|
||||
* @csspart prefix - The container that wraps the prefix.
|
||||
* @csspart suffix - The container that wraps the suffix.
|
||||
* @csspart separator - The container that wraps the separator.
|
||||
*/
|
||||
@customElement('sl-breadcrumb-item')
|
||||
export default class SlBreadcrumbItem extends ShoelaceElement {
|
||||
@@ -55,9 +55,7 @@ export default class SlBreadcrumbItem extends ShoelaceElement {
|
||||
'breadcrumb-item--has-suffix': this.hasSlotController.test('suffix')
|
||||
})}
|
||||
>
|
||||
<span part="prefix" class="breadcrumb-item__prefix">
|
||||
<slot name="prefix"></slot>
|
||||
</span>
|
||||
<slot name="prefix" part="prefix" class="breadcrumb-item__prefix"></slot>
|
||||
|
||||
${isLink
|
||||
? html`
|
||||
@@ -77,13 +75,9 @@ export default class SlBreadcrumbItem extends ShoelaceElement {
|
||||
</button>
|
||||
`}
|
||||
|
||||
<span part="suffix" class="breadcrumb-item__suffix">
|
||||
<slot name="suffix"></slot>
|
||||
</span>
|
||||
<slot name="suffix" part="suffix" class="breadcrumb-item__suffix"></slot>
|
||||
|
||||
<span part="separator" class="breadcrumb-item__separator" aria-hidden="true">
|
||||
<slot name="separator"></slot>
|
||||
</span>
|
||||
<slot name="separator" part="separator" class="breadcrumb-item__separator" aria-hidden="true"></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
import type SlBreadcrumb from './breadcrumb';
|
||||
|
||||
// The default link color just misses AA contrast, but the next step up is way too dark. Maybe we can solve this in the
|
||||
// future with a prefers-contrast media query.
|
||||
const ignoredRules = ['color-contrast'];
|
||||
|
||||
describe('<sl-breadcrumb>', () => {
|
||||
let el: SlBreadcrumb;
|
||||
|
||||
@@ -17,7 +21,7 @@ describe('<sl-breadcrumb>', () => {
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
await expect(el).to.be.accessible({ ignoredRules });
|
||||
});
|
||||
|
||||
it('should render sl-icon as separator', () => {
|
||||
@@ -44,7 +48,7 @@ describe('<sl-breadcrumb>', () => {
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
await expect(el).to.be.accessible({ ignoredRules });
|
||||
});
|
||||
|
||||
it('should accept "separator" as an assigned child in the shadow root', () => {
|
||||
@@ -76,7 +80,7 @@ describe('<sl-breadcrumb>', () => {
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
await expect(el).to.be.accessible({ ignoredRules });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -96,7 +100,7 @@ describe('<sl-breadcrumb>', () => {
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
await expect(el).to.be.accessible({ ignoredRules });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import '../icon/icon';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { html } from 'lit';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import styles from './breadcrumb.styles';
|
||||
import type SlBreadcrumbItem from '../breadcrumb-item/breadcrumb-item';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
import type SlBreadcrumbItem from '../breadcrumb-item/breadcrumb-item';
|
||||
|
||||
/**
|
||||
* @summary Breadcrumbs provide a group of links so users can easily navigate a website's hierarchy.
|
||||
*
|
||||
* @since 2.0
|
||||
* @documentation https://shoelace.style/components/breadcrumb
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @slot - One or more breadcrumb items to display.
|
||||
* @slot separator - The separator to use between breadcrumb items.
|
||||
* @slot separator - The separator to use between breadcrumb items. Works best with `<sl-icon>`.
|
||||
*
|
||||
* @dependency sl-icon
|
||||
*
|
||||
* @csspart base - The component's internal wrapper.
|
||||
* @csspart base - The component's base wrapper.
|
||||
*/
|
||||
@customElement('sl-breadcrumb')
|
||||
export default class SlBreadcrumb extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
@query('slot') defaultSlot: HTMLSlotElement;
|
||||
@query('slot[name="separator"]') separatorSlot: HTMLSlotElement;
|
||||
|
||||
private readonly localize = new LocalizeController(this);
|
||||
private separatorDir = this.localize.dir();
|
||||
|
||||
@query('slot') defaultSlot: HTMLSlotElement;
|
||||
@query('slot[name="separator"]') separatorSlot: HTMLSlotElement;
|
||||
|
||||
/**
|
||||
* The label to use for the breadcrumb control. This will not be shown, but it will be announced by screen readers and
|
||||
* other assistive devices.
|
||||
* The label to use for the breadcrumb control. This will not be shown on the screen, but it will be announced by
|
||||
* screen readers and other assistive devices to provide more context for users.
|
||||
*/
|
||||
@property() label = 'Breadcrumb';
|
||||
@property() label = '';
|
||||
|
||||
// Generates a clone of the separator element to use for each breadcrumb item
|
||||
private getSeparator() {
|
||||
@@ -49,7 +49,7 @@ export default class SlBreadcrumb extends ShoelaceElement {
|
||||
return clone;
|
||||
}
|
||||
|
||||
handleSlotChange() {
|
||||
private handleSlotChange() {
|
||||
const items = [...this.defaultSlot.assignedElements({ flatten: true })].filter(
|
||||
item => item.tagName.toLowerCase() === 'sl-breadcrumb-item'
|
||||
) as SlBreadcrumbItem[];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, fixture, html, elementUpdated } from '@open-wc/testing';
|
||||
import { elementUpdated, expect, fixture, html } from '@open-wc/testing';
|
||||
import type SlButtonGroup from './button-group';
|
||||
|
||||
describe('<sl-button-group>', () => {
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { html } from 'lit';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import styles from './button-group.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
* @summary Button groups can be used to group related buttons into sections.
|
||||
*
|
||||
* @since 2.0
|
||||
* @documentation https://shoelace.style/components/button-group
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @slot - One or more `<sl-button>` elements to display in the button group.
|
||||
*
|
||||
* @csspart base - The component's internal wrapper.
|
||||
* @csspart base - The component's base wrapper.
|
||||
*/
|
||||
@customElement('sl-button-group')
|
||||
export default class SlButtonGroup extends ShoelaceElement {
|
||||
@@ -22,30 +22,33 @@ export default class SlButtonGroup extends ShoelaceElement {
|
||||
|
||||
@state() disableRole = false;
|
||||
|
||||
/** A label to use for the button group's `aria-label` attribute. */
|
||||
/**
|
||||
* A label to use for the button group. This won't be displayed on the screen, but it will be announced by assistive
|
||||
* devices when interacting with the control and is strongly recommended.
|
||||
*/
|
||||
@property() label = '';
|
||||
|
||||
handleFocus(event: CustomEvent) {
|
||||
private handleFocus(event: CustomEvent) {
|
||||
const button = findButton(event.target as HTMLElement);
|
||||
button?.classList.add('sl-button-group__button--focus');
|
||||
}
|
||||
|
||||
handleBlur(event: CustomEvent) {
|
||||
private handleBlur(event: CustomEvent) {
|
||||
const button = findButton(event.target as HTMLElement);
|
||||
button?.classList.remove('sl-button-group__button--focus');
|
||||
}
|
||||
|
||||
handleMouseOver(event: CustomEvent) {
|
||||
private handleMouseOver(event: CustomEvent) {
|
||||
const button = findButton(event.target as HTMLElement);
|
||||
button?.classList.add('sl-button-group__button--hover');
|
||||
}
|
||||
|
||||
handleMouseOut(event: CustomEvent) {
|
||||
private handleMouseOut(event: CustomEvent) {
|
||||
const button = findButton(event.target as HTMLElement);
|
||||
button?.classList.remove('sl-button-group__button--hover');
|
||||
}
|
||||
|
||||
handleSlotChange() {
|
||||
private handleSlotChange() {
|
||||
const slottedElements = [...this.defaultSlot.assignedElements({ flatten: true })] as HTMLElement[];
|
||||
|
||||
slottedElements.forEach(el => {
|
||||
@@ -63,9 +66,9 @@ export default class SlButtonGroup extends ShoelaceElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
// eslint-disable-next-line lit-a11y/mouse-events-have-key-events -- focusout & focusin support bubbling whereas focus & blur do not which is necessary here
|
||||
// eslint-disable-next-line lit-a11y/mouse-events-have-key-events
|
||||
return html`
|
||||
<div
|
||||
<slot
|
||||
part="base"
|
||||
class="button-group"
|
||||
role="${this.disableRole ? 'presentation' : 'group'}"
|
||||
@@ -74,16 +77,17 @@ export default class SlButtonGroup extends ShoelaceElement {
|
||||
@focusin=${this.handleFocus}
|
||||
@mouseover=${this.handleMouseOver}
|
||||
@mouseout=${this.handleMouseOut}
|
||||
>
|
||||
<slot @slotchange=${this.handleSlotChange} role="none"></slot>
|
||||
</div>
|
||||
@slotchange=${this.handleSlotChange}
|
||||
></slot>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function findButton(el: HTMLElement) {
|
||||
const children = ['sl-button', 'sl-radio-button'];
|
||||
return children.includes(el.tagName.toLowerCase()) ? el : el.querySelector(children.join(','));
|
||||
const selector = 'sl-button, sl-radio-button';
|
||||
|
||||
// The button could be the target element or a child of it (e.g. a dropdown or tooltip anchor)
|
||||
return el.closest(selector) ?? el.querySelector(selector);
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -61,7 +61,11 @@ export default css`
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.button__label ::slotted(sl-icon) {
|
||||
.button__label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.button__label::slotted(sl-icon) {
|
||||
vertical-align: -2px;
|
||||
}
|
||||
|
||||
@@ -451,14 +455,14 @@ export default css`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
transform: translateY(-50%) translateX(50%);
|
||||
translate: 50% -50%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.button--rtl ::slotted(sl-badge) {
|
||||
right: auto;
|
||||
left: 0;
|
||||
transform: translateY(-50%) translateX(-50%);
|
||||
translate: -50% -50%;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -558,7 +562,13 @@ export default css`
|
||||
}
|
||||
|
||||
/* Add a visual separator between solid buttons */
|
||||
:host(.sl-button-group__button:not(.sl-button-group__button--first, .sl-button-group__button--radio, [variant='default']):not(:hover))
|
||||
:host(
|
||||
.sl-button-group__button:not(
|
||||
.sl-button-group__button--first,
|
||||
.sl-button-group__button--radio,
|
||||
[variant='default']
|
||||
):not(:hover)
|
||||
)
|
||||
.button:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -574,6 +584,8 @@ export default css`
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Focus and checked are always on top */
|
||||
:host(.sl-button-group__button--focus),
|
||||
:host(.sl-button-group__button[checked]) {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ describe('<sl-button>', () => {
|
||||
it('default values are set correctly', async () => {
|
||||
const el = await fixture<SlButton>(html` <sl-button>Button Label</sl-button> `);
|
||||
|
||||
expect(el.title).to.equal('');
|
||||
expect(el.variant).to.equal('default');
|
||||
expect(el.size).to.equal('medium');
|
||||
expect(el.disabled).to.equal(false);
|
||||
@@ -88,6 +89,13 @@ describe('<sl-button>', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should have title if title attribute is set', async () => {
|
||||
const el = await fixture<SlButton>(html` <sl-button title="Test"></sl-button> `);
|
||||
const button = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="base"]')!;
|
||||
|
||||
expect(button.title).to.equal('Test');
|
||||
});
|
||||
|
||||
describe('when loading', () => {
|
||||
it('should have a spinner present', async () => {
|
||||
const el = await fixture<SlButton>(html` <sl-button loading>Button Label</sl-button> `);
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { html, literal } from 'lit/static-html.js';
|
||||
import { FormSubmitController } from '../../internal/form';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import '../icon/icon';
|
||||
import '../spinner/spinner';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { FormControlController } from '../../internal/form';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { html, literal } from 'lit/static-html.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import { watch } from '../../internal/watch';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import styles from './button.styles';
|
||||
import type { ShoelaceFormControl } from '../../internal/shoelace-element';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
import type { ShoelaceFormControl } from '../../internal/shoelace-element';
|
||||
|
||||
/**
|
||||
* @summary Buttons represent actions that are available to the user.
|
||||
*
|
||||
* @since 2.0
|
||||
* @documentation https://shoelace.style/components/button
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @dependency sl-icon
|
||||
* @dependency sl-spinner
|
||||
@@ -26,22 +26,20 @@ import type { CSSResultGroup } from 'lit';
|
||||
* @event sl-focus - Emitted when the button gains focus.
|
||||
*
|
||||
* @slot - The button's label.
|
||||
* @slot prefix - Used to prepend an icon or similar element to the button.
|
||||
* @slot suffix - Used to append an icon or similar element to the button.
|
||||
* @slot prefix - A presentational prefix icon or similar element.
|
||||
* @slot suffix - A presentational suffix icon or similar element.
|
||||
*
|
||||
* @csspart base - The component's internal wrapper.
|
||||
* @csspart prefix - The prefix slot's container.
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart prefix - The container that wraps the prefix.
|
||||
* @csspart label - The button's label.
|
||||
* @csspart suffix - The suffix slot's container.
|
||||
* @csspart caret - The button's caret icon.
|
||||
* @csspart suffix - The container that wraps the suffix.
|
||||
* @csspart caret - The button's caret icon, an `<sl-icon>` element.
|
||||
*/
|
||||
@customElement('sl-button')
|
||||
export default class SlButton extends ShoelaceElement implements ShoelaceFormControl {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
@query('.button') button: HTMLButtonElement | HTMLLinkElement;
|
||||
|
||||
private readonly formSubmitController = new FormSubmitController(this, {
|
||||
private readonly formControlController = new FormControlController(this, {
|
||||
form: input => {
|
||||
// Buttons support a form attribute that points to an arbitrary form, so if this attribute it set we need to query
|
||||
// the form from the same root using its id
|
||||
@@ -58,17 +56,20 @@ export default class SlButton extends ShoelaceElement implements ShoelaceFormCon
|
||||
private readonly hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix');
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
@query('.button') button: HTMLButtonElement | HTMLLinkElement;
|
||||
|
||||
@state() private hasFocus = false;
|
||||
@state() invalid = false;
|
||||
@property() title = ''; // make reactive to pass through
|
||||
|
||||
/** The button's variant. */
|
||||
/** The button's theme variant. */
|
||||
@property({ reflect: true }) variant: 'default' | 'primary' | 'success' | 'neutral' | 'warning' | 'danger' | 'text' =
|
||||
'default';
|
||||
|
||||
/** The button's size. */
|
||||
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
|
||||
|
||||
/** Draws the button with a caret for use with dropdowns, popovers, etc. */
|
||||
/** Draws the button with a caret. Used to indicate that the button triggers a dropdown menu or similar behavior. */
|
||||
@property({ type: Boolean, reflect: true }) caret = false;
|
||||
|
||||
/** Disables the button. */
|
||||
@@ -83,28 +84,37 @@ export default class SlButton extends ShoelaceElement implements ShoelaceFormCon
|
||||
/** Draws a pill-style button with rounded edges. */
|
||||
@property({ type: Boolean, reflect: true }) pill = false;
|
||||
|
||||
/** Draws a circle button. */
|
||||
/**
|
||||
* Draws a circular icon button. When this attribute is present, the button expects a single `<sl-icon>` in the
|
||||
* default slot.
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true }) circle = false;
|
||||
|
||||
/**
|
||||
* The type of button. When the type is `submit`, the button will submit the surrounding form. Note that the default
|
||||
* value is `button` instead of `submit`, which is opposite of how native `<button>` elements behave.
|
||||
* The type of button. Note that the default value is `button` instead of `submit`, which is opposite of how native
|
||||
* `<button>` elements behave. When the type is `submit`, the button will submit the surrounding form.
|
||||
*/
|
||||
@property() type: 'button' | 'submit' | 'reset' = 'button';
|
||||
|
||||
/** An optional name for the button. Ignored when `href` is set. */
|
||||
/**
|
||||
* The name of the button, submitted as a name/value pair with form data, but only when this button is the submitter.
|
||||
* This attribute is ignored when `href` is present.
|
||||
*/
|
||||
@property() name = '';
|
||||
|
||||
/** An optional value for the button. Ignored when `href` is set. */
|
||||
/**
|
||||
* The value of the button, submitted as a pair with the button's name as part of the form data, but only when this
|
||||
* button is the submitter. This attribute is ignored when `href` is present.
|
||||
*/
|
||||
@property() value = '';
|
||||
|
||||
/** When set, the underlying button will be rendered as an `<a>` with this `href` instead of a `<button>`. */
|
||||
@property() href = '';
|
||||
|
||||
/** Tells the browser where to open the link. Only used when `href` is set. */
|
||||
/** Tells the browser where to open the link. Only used when `href` is present. */
|
||||
@property() target: '_blank' | '_parent' | '_self' | '_top';
|
||||
|
||||
/** Tells the browser to download the linked file as this filename. Only used when `href` is set. */
|
||||
/** Tells the browser to download the linked file as this filename. Only used when `href` is present. */
|
||||
@property() download?: string;
|
||||
|
||||
/**
|
||||
@@ -131,7 +141,49 @@ export default class SlButton extends ShoelaceElement implements ShoelaceFormCon
|
||||
|
||||
firstUpdated() {
|
||||
if (this.isButton()) {
|
||||
this.invalid = !(this.button as HTMLButtonElement).checkValidity();
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
}
|
||||
|
||||
private handleBlur() {
|
||||
this.hasFocus = false;
|
||||
this.emit('sl-blur');
|
||||
}
|
||||
|
||||
private handleFocus() {
|
||||
this.hasFocus = true;
|
||||
this.emit('sl-focus');
|
||||
}
|
||||
|
||||
private handleClick(event: MouseEvent) {
|
||||
if (this.disabled || this.loading) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.type === 'submit') {
|
||||
this.formControlController.submit(this);
|
||||
}
|
||||
|
||||
if (this.type === 'reset') {
|
||||
this.formControlController.reset(this);
|
||||
}
|
||||
}
|
||||
|
||||
private isButton() {
|
||||
return this.href ? false : true;
|
||||
}
|
||||
|
||||
private isLink() {
|
||||
return this.href ? true : false;
|
||||
}
|
||||
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
if (this.isButton()) {
|
||||
// Disabled form controls are always valid
|
||||
this.formControlController.setValidity(this.disabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,57 +220,14 @@ export default class SlButton extends ShoelaceElement implements ShoelaceFormCon
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
|
||||
/** Sets a custom validation message. Pass an empty string to restore validity. */
|
||||
setCustomValidity(message: string) {
|
||||
if (this.isButton()) {
|
||||
(this.button as HTMLButtonElement).setCustomValidity(message);
|
||||
this.invalid = !(this.button as HTMLButtonElement).checkValidity();
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
}
|
||||
|
||||
handleBlur() {
|
||||
this.hasFocus = false;
|
||||
this.emit('sl-blur');
|
||||
}
|
||||
|
||||
handleFocus() {
|
||||
this.hasFocus = true;
|
||||
this.emit('sl-focus');
|
||||
}
|
||||
|
||||
handleClick(event: MouseEvent) {
|
||||
if (this.disabled || this.loading) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.type === 'submit') {
|
||||
this.formSubmitController.submit(this);
|
||||
}
|
||||
|
||||
if (this.type === 'reset') {
|
||||
this.formSubmitController.reset(this);
|
||||
}
|
||||
}
|
||||
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
// Disabled form controls are always valid, so we need to recheck validity when the state changes
|
||||
if (this.isButton()) {
|
||||
this.button.disabled = this.disabled;
|
||||
this.invalid = !(this.button as HTMLButtonElement).checkValidity();
|
||||
}
|
||||
}
|
||||
|
||||
private isButton() {
|
||||
return this.href ? false : true;
|
||||
}
|
||||
|
||||
private isLink() {
|
||||
return this.href ? true : false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const isLink = this.isLink();
|
||||
const tag = isLink ? literal`a` : literal`button`;
|
||||
@@ -255,6 +264,7 @@ export default class SlButton extends ShoelaceElement implements ShoelaceFormCon
|
||||
})}
|
||||
?disabled=${ifDefined(isLink ? undefined : this.disabled)}
|
||||
type=${ifDefined(isLink ? undefined : this.type)}
|
||||
title=${this.title /* An empty title prevents browser validation tooltips from appearing on hover */}
|
||||
name=${ifDefined(isLink ? undefined : this.name)}
|
||||
value=${ifDefined(isLink ? undefined : this.value)}
|
||||
href=${ifDefined(isLink ? this.href : undefined)}
|
||||
@@ -268,15 +278,9 @@ export default class SlButton extends ShoelaceElement implements ShoelaceFormCon
|
||||
@focus=${this.handleFocus}
|
||||
@click=${this.handleClick}
|
||||
>
|
||||
<span part="prefix" class="button__prefix">
|
||||
<slot name="prefix"></slot>
|
||||
</span>
|
||||
<span part="label" class="button__label">
|
||||
<slot></slot>
|
||||
</span>
|
||||
<span part="suffix" class="button__suffix">
|
||||
<slot name="suffix"></slot>
|
||||
</span>
|
||||
<slot name="prefix" part="prefix" class="button__prefix"></slot>
|
||||
<slot part="label" class="button__label"></slot>
|
||||
<slot name="suffix" part="suffix" class="button__suffix"></slot>
|
||||
${
|
||||
this.caret ? html` <sl-icon part="caret" class="button__caret" library="system" name="caret"></sl-icon> ` : ''
|
||||
}
|
||||
|
||||
@@ -23,13 +23,14 @@ export default css`
|
||||
}
|
||||
|
||||
.card__image {
|
||||
display: flex;
|
||||
border-top-left-radius: var(--border-radius);
|
||||
border-top-right-radius: var(--border-radius);
|
||||
margin: calc(-1 * var(--border-width));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card__image ::slotted(img) {
|
||||
.card__image::slotted(img) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -39,6 +40,7 @@ export default css`
|
||||
}
|
||||
|
||||
.card__header {
|
||||
display: block;
|
||||
border-bottom: solid var(--border-width) var(--border-color);
|
||||
padding: calc(var(--padding) / 2) var(--padding);
|
||||
}
|
||||
@@ -53,10 +55,12 @@ export default css`
|
||||
}
|
||||
|
||||
.card__body {
|
||||
display: block;
|
||||
padding: var(--padding);
|
||||
}
|
||||
|
||||
.card--has-footer .card__footer {
|
||||
display: block;
|
||||
border-top: solid var(--border-width) var(--border-color);
|
||||
padding: var(--padding);
|
||||
}
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { html } from 'lit';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import styles from './card.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
* @summary Cards can be used to group related subjects in a container.
|
||||
*
|
||||
* @since 2.0
|
||||
* @documentation https://shoelace.style/components/card
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @slot - The card's body.
|
||||
* @slot header - The card's header.
|
||||
* @slot footer - The card's footer.
|
||||
* @slot image - The card's image.
|
||||
* @slot - The card's main content.
|
||||
* @slot header - An optional header for the card.
|
||||
* @slot footer - An optional footer for the card.
|
||||
* @slot image - An optional image to render at the start of the card.
|
||||
*
|
||||
* @csspart base - The component's internal wrapper.
|
||||
* @csspart image - The card's image, if present.
|
||||
* @csspart header - The card's header, if present.
|
||||
* @csspart body - The card's body.
|
||||
* @csspart footer - The card's footer, if present.
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart image - The container that wraps the card's image.
|
||||
* @csspart header - The container that wraps the card's header.
|
||||
* @csspart body - The container that wraps the card's main content.
|
||||
* @csspart footer - The container that wraps the card's footer.
|
||||
*
|
||||
* @cssproperty --border-color - The card's border color, including borders that occur inside the card.
|
||||
* @cssproperty --border-radius - The border radius for card edges.
|
||||
* @cssproperty --border-width - The width of card borders.
|
||||
* @cssproperty --padding - The padding to use for card sections.*
|
||||
* @cssproperty --border-radius - The border radius for the card's edges.
|
||||
* @cssproperty --border-width - The width of the card's borders.
|
||||
* @cssproperty --padding - The padding to use for the card's sections.
|
||||
*/
|
||||
@customElement('sl-card')
|
||||
export default class SlCard extends ShoelaceElement {
|
||||
@@ -45,21 +45,10 @@ export default class SlCard extends ShoelaceElement {
|
||||
'card--has-header': this.hasSlotController.test('header')
|
||||
})}
|
||||
>
|
||||
<div part="image" class="card__image">
|
||||
<slot name="image"></slot>
|
||||
</div>
|
||||
|
||||
<div part="header" class="card__header">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
|
||||
<div part="body" class="card__body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<div part="footer" class="card__footer">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
<slot name="image" part="image" class="card__image"></slot>
|
||||
<slot name="header" part="header" class="card__header"></slot>
|
||||
<slot part="body" class="card__body"></slot>
|
||||
<slot name="footer" part="footer" class="card__footer"></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -12,21 +12,35 @@ export default css`
|
||||
display: inline-flex;
|
||||
align-items: top;
|
||||
font-family: var(--sl-input-font-family);
|
||||
font-size: var(--sl-input-font-size-medium);
|
||||
font-weight: var(--sl-input-font-weight);
|
||||
color: var(--sl-input-color);
|
||||
color: var(--sl-input-label-color);
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox--small {
|
||||
--toggle-size: var(--sl-toggle-size-small);
|
||||
font-size: var(--sl-input-font-size-small);
|
||||
}
|
||||
|
||||
.checkbox--medium {
|
||||
--toggle-size: var(--sl-toggle-size-medium);
|
||||
font-size: var(--sl-input-font-size-medium);
|
||||
}
|
||||
|
||||
.checkbox--large {
|
||||
--toggle-size: var(--sl-toggle-size-large);
|
||||
font-size: var(--sl-input-font-size-large);
|
||||
}
|
||||
|
||||
.checkbox__control {
|
||||
flex: 0 0 auto;
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--sl-toggle-size);
|
||||
height: var(--sl-toggle-size);
|
||||
width: var(--toggle-size);
|
||||
height: var(--toggle-size);
|
||||
border: solid var(--sl-input-border-width) var(--sl-input-border-color);
|
||||
border-radius: 2px;
|
||||
background-color: var(--sl-input-background-color);
|
||||
@@ -43,10 +57,11 @@ export default css`
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.checkbox__control .checkbox__icon {
|
||||
.checkbox__checked-icon,
|
||||
.checkbox__indeterminate-icon {
|
||||
display: inline-flex;
|
||||
width: var(--sl-toggle-size);
|
||||
height: var(--sl-toggle-size);
|
||||
width: var(--toggle-size);
|
||||
height: var(--toggle-size);
|
||||
}
|
||||
|
||||
/* Hover */
|
||||
@@ -89,8 +104,9 @@ export default css`
|
||||
}
|
||||
|
||||
.checkbox__label {
|
||||
display: inline-block;
|
||||
color: var(--sl-input-label-color);
|
||||
line-height: var(--sl-toggle-size);
|
||||
line-height: var(--toggle-size);
|
||||
margin-inline-start: 0.5em;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,35 @@
|
||||
import { aTimeout, expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
|
||||
import { clickOnElement } from '../../internal/test';
|
||||
import { expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import sinon from 'sinon';
|
||||
import type SlCheckbox from './checkbox';
|
||||
|
||||
describe('<sl-checkbox>', () => {
|
||||
it('should pass accessibility tests', async () => {
|
||||
const el = await fixture<SlCheckbox>(html` <sl-checkbox>Checkbox</sl-checkbox> `);
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('default properties', async () => {
|
||||
const el = await fixture<SlCheckbox>(html` <sl-checkbox></sl-checkbox> `);
|
||||
|
||||
expect(el.name).to.equal('');
|
||||
expect(el.value).to.be.undefined;
|
||||
expect(el.title).to.equal('');
|
||||
expect(el.disabled).to.be.false;
|
||||
expect(el.required).to.be.false;
|
||||
expect(el.checked).to.be.false;
|
||||
expect(el.indeterminate).to.be.false;
|
||||
expect(el.defaultChecked).to.be.false;
|
||||
});
|
||||
|
||||
it('should have title if title attribute is set', async () => {
|
||||
const el = await fixture<SlCheckbox>(html` <sl-checkbox title="Test"></sl-checkbox> `);
|
||||
const input = el.shadowRoot!.querySelector('input')!;
|
||||
|
||||
expect(input.title).to.equal('Test');
|
||||
});
|
||||
|
||||
it('should be disabled with the disabled attribute', async () => {
|
||||
const el = await fixture<SlCheckbox>(html` <sl-checkbox disabled></sl-checkbox> `);
|
||||
const checkbox = el.shadowRoot!.querySelector('input')!;
|
||||
@@ -23,31 +49,45 @@ describe('<sl-checkbox>', () => {
|
||||
|
||||
it('should be valid by default', async () => {
|
||||
const el = await fixture<SlCheckbox>(html` <sl-checkbox></sl-checkbox> `);
|
||||
|
||||
expect(el.invalid).to.be.false;
|
||||
expect(el.checkValidity()).to.be.true;
|
||||
});
|
||||
|
||||
it('should fire sl-change when clicked', async () => {
|
||||
it('should emit sl-change and sl-input when clicked', async () => {
|
||||
const el = await fixture<SlCheckbox>(html` <sl-checkbox></sl-checkbox> `);
|
||||
setTimeout(() => el.shadowRoot!.querySelector('input')!.click());
|
||||
const event = (await oneEvent(el, 'sl-change')) as CustomEvent;
|
||||
expect(event.target).to.equal(el);
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
el.click();
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
expect(el.checked).to.be.true;
|
||||
});
|
||||
|
||||
it('should fire sl-change when toggled via keyboard', async () => {
|
||||
it('should emit sl-change and sl-input when toggled with spacebar', async () => {
|
||||
const el = await fixture<SlCheckbox>(html` <sl-checkbox></sl-checkbox> `);
|
||||
const input = el.shadowRoot!.querySelector('input')!;
|
||||
input.focus();
|
||||
setTimeout(() => sendKeys({ press: ' ' }));
|
||||
const event = (await oneEvent(el, 'sl-change')) as CustomEvent;
|
||||
expect(event.target).to.equal(el);
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
el.focus();
|
||||
await el.updateComplete;
|
||||
await sendKeys({ press: ' ' });
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
expect(el.checked).to.be.true;
|
||||
});
|
||||
|
||||
it('should not fire sl-change when checked is set by javascript', async () => {
|
||||
it('should not emit sl-change or sl-input when checked programmatically', async () => {
|
||||
const el = await fixture<SlCheckbox>(html` <sl-checkbox></sl-checkbox> `);
|
||||
el.addEventListener('sl-change', () => expect.fail('event fired'));
|
||||
|
||||
el.addEventListener('sl-change', () => expect.fail('sl-change should not be emitted'));
|
||||
el.addEventListener('sl-input', () => expect.fail('sl-input should not be emitted'));
|
||||
el.checked = true;
|
||||
await el.updateComplete;
|
||||
el.checked = false;
|
||||
@@ -99,25 +139,50 @@ describe('<sl-checkbox>', () => {
|
||||
expect(formData!.get('a')).to.equal('on');
|
||||
});
|
||||
|
||||
it('should show a constraint validation error when setCustomValidity() is called', async () => {
|
||||
const form = await fixture<HTMLFormElement>(html`
|
||||
<form>
|
||||
<sl-checkbox name="a" value="1" checked></sl-checkbox>
|
||||
<sl-button type="submit">Submit</sl-button>
|
||||
</form>
|
||||
`);
|
||||
const button = form.querySelector('sl-button')!;
|
||||
const checkbox = form.querySelector('sl-checkbox')!;
|
||||
const submitHandler = sinon.spy((event: SubmitEvent) => event.preventDefault());
|
||||
it('should be invalid when setCustomValidity() is called with a non-empty value', async () => {
|
||||
const checkbox = await fixture<HTMLFormElement>(html` <sl-checkbox></sl-checkbox> `);
|
||||
|
||||
// Submitting the form after setting custom validity should not trigger the handler
|
||||
checkbox.setCustomValidity('Invalid selection');
|
||||
form.addEventListener('submit', submitHandler);
|
||||
button.click();
|
||||
await checkbox.updateComplete;
|
||||
|
||||
await aTimeout(100);
|
||||
expect(checkbox.checkValidity()).to.be.false;
|
||||
expect(checkbox.checkValidity()).to.be.false;
|
||||
expect(checkbox.hasAttribute('data-invalid')).to.be.true;
|
||||
expect(checkbox.hasAttribute('data-valid')).to.be.false;
|
||||
expect(checkbox.hasAttribute('data-user-invalid')).to.be.false;
|
||||
expect(checkbox.hasAttribute('data-user-valid')).to.be.false;
|
||||
|
||||
expect(submitHandler).to.not.have.been.called;
|
||||
await clickOnElement(checkbox);
|
||||
await checkbox.updateComplete;
|
||||
|
||||
expect(checkbox.hasAttribute('data-user-invalid')).to.be.true;
|
||||
expect(checkbox.hasAttribute('data-user-valid')).to.be.false;
|
||||
});
|
||||
|
||||
it('should be invalid when required and unchecked', async () => {
|
||||
const checkbox = await fixture<HTMLFormElement>(html` <sl-checkbox required></sl-checkbox> `);
|
||||
expect(checkbox.checkValidity()).to.be.false;
|
||||
});
|
||||
|
||||
it('should be valid when required and checked', async () => {
|
||||
const checkbox = await fixture<HTMLFormElement>(html` <sl-checkbox required checked></sl-checkbox> `);
|
||||
expect(checkbox.checkValidity()).to.be.true;
|
||||
});
|
||||
|
||||
it('should be present in form data when using the form attribute and located outside of a <form>', async () => {
|
||||
const el = await fixture<HTMLFormElement>(html`
|
||||
<div>
|
||||
<form id="f">
|
||||
<sl-button type="submit">Submit</sl-button>
|
||||
</form>
|
||||
<sl-checkbox form="f" name="a" value="1" checked></sl-checkbox>
|
||||
</div>
|
||||
`);
|
||||
const form = el.querySelector('form')!;
|
||||
const formData = new FormData(form);
|
||||
|
||||
expect(formData.get('a')).to.equal('1');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,76 +1,125 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import '../icon/icon';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { defaultValue } from '../../internal/default-value';
|
||||
import { FormControlController } from '../../internal/form';
|
||||
import { html } from 'lit';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { live } from 'lit/directives/live.js';
|
||||
import { defaultValue } from '../../internal/default-value';
|
||||
import { FormSubmitController } from '../../internal/form';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { watch } from '../../internal/watch';
|
||||
import '../icon/icon';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import styles from './checkbox.styles';
|
||||
import type { ShoelaceFormControl } from '../../internal/shoelace-element';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
import type { ShoelaceFormControl } from '../../internal/shoelace-element';
|
||||
|
||||
/**
|
||||
* @summary Checkboxes allow the user to toggle an option on or off.
|
||||
*
|
||||
* @since 2.0
|
||||
* @documentation https://shoelace.style/components/checkbox
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @dependency sl-icon
|
||||
*
|
||||
* @slot - The checkbox's label.
|
||||
*
|
||||
* @event sl-blur - Emitted when the control loses focus.
|
||||
* @event sl-change - Emitted when the control's checked state changes.
|
||||
* @event sl-focus - Emitted when the control gains focus.
|
||||
* @event sl-blur - Emitted when the checkbox loses focus.
|
||||
* @event sl-change - Emitted when the checked state changes.
|
||||
* @event sl-focus - Emitted when the checkbox gains focus.
|
||||
* @event sl-input - Emitted when the checkbox receives input.
|
||||
*
|
||||
* @csspart base - The component's internal wrapper.
|
||||
* @csspart control - The checkbox control.
|
||||
* @csspart checked-icon - The checked icon.
|
||||
* @csspart indeterminate-icon - The indeterminate icon.
|
||||
* @csspart label - The checkbox label.
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart control - The square container that wraps the checkbox's checked state.
|
||||
* @csspart control--checked - Matches the control part when the checkbox is checked.
|
||||
* @csspart control--indeterminate - Matches the control part when the checkbox is indeterminate.
|
||||
* @csspart checked-icon - The checked icon, an `<sl-icon>` element.
|
||||
* @csspart indeterminate-icon - The indeterminate icon, an `<sl-icon>` element.
|
||||
* @csspart label - The container that wraps the checkbox's label.
|
||||
*/
|
||||
@customElement('sl-checkbox')
|
||||
export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormControl {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
@query('input[type="checkbox"]') input: HTMLInputElement;
|
||||
|
||||
// @ts-expect-error -- Controller is currently unused
|
||||
private readonly formSubmitController = new FormSubmitController(this, {
|
||||
private readonly formControlController = new FormControlController(this, {
|
||||
value: (control: SlCheckbox) => (control.checked ? control.value || 'on' : undefined),
|
||||
defaultValue: (control: SlCheckbox) => control.defaultChecked,
|
||||
setValue: (control: SlCheckbox, checked: boolean) => (control.checked = checked)
|
||||
});
|
||||
|
||||
@query('input[type="checkbox"]') input: HTMLInputElement;
|
||||
|
||||
@state() private hasFocus = false;
|
||||
@state() invalid = false;
|
||||
|
||||
/** Name of the HTML form control. Submitted with the form as part of a name/value pair. */
|
||||
@property() name: string;
|
||||
@property() title = ''; // make reactive to pass through
|
||||
|
||||
/** Value of the HTML form control. Primarily used to differentiate a list of related checkboxes that have the same name. */
|
||||
/** The name of the checkbox, submitted as a name/value pair with form data. */
|
||||
@property() name = '';
|
||||
|
||||
/** The current value of the checkbox, submitted as a name/value pair with form data. */
|
||||
@property() value: string;
|
||||
|
||||
/** Disables the checkbox (so the user can't check / uncheck it). */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
/** The checkbox's size. */
|
||||
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
|
||||
|
||||
/** Makes the checkbox a required field. */
|
||||
@property({ type: Boolean, reflect: true }) required = false;
|
||||
/** Disables the checkbox. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
/** Draws the checkbox in a checked state. */
|
||||
@property({ type: Boolean, reflect: true }) checked = false;
|
||||
|
||||
/** Draws the checkbox in an indeterminate state. Usually applies to a checkbox that represents "select all" or "select none" when the items to which it applies are a mix of selected and unselected. */
|
||||
/**
|
||||
* Draws the checkbox in an indeterminate state. This is usually applied to checkboxes that represents a "select
|
||||
* all/none" behavior when associated checkboxes have a mix of checked and unchecked states.
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true }) indeterminate = false;
|
||||
|
||||
/** Gets or sets the default value used to reset this element. The initial value corresponds to the one originally specified in the HTML that created this element. */
|
||||
/** The default value of the form control. Primarily used for resetting the form control. */
|
||||
@defaultValue('checked') defaultChecked = false;
|
||||
|
||||
/**
|
||||
* By default, form controls are associated with the nearest containing `<form>` element. This attribute allows you
|
||||
* to place the form control outside of a form and associate it with the form that has this `id`. The form must be in
|
||||
* the same document or shadow root for this to work.
|
||||
*/
|
||||
@property({ reflect: true }) form = '';
|
||||
|
||||
/** Makes the checkbox a required field. */
|
||||
@property({ type: Boolean, reflect: true }) required = false;
|
||||
|
||||
firstUpdated() {
|
||||
this.invalid = !this.input.checkValidity();
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
|
||||
private handleClick() {
|
||||
this.checked = !this.checked;
|
||||
this.indeterminate = false;
|
||||
this.emit('sl-change');
|
||||
}
|
||||
|
||||
private handleBlur() {
|
||||
this.hasFocus = false;
|
||||
this.emit('sl-blur');
|
||||
}
|
||||
|
||||
private handleInput() {
|
||||
this.emit('sl-input');
|
||||
}
|
||||
|
||||
private handleFocus() {
|
||||
this.hasFocus = true;
|
||||
this.emit('sl-focus');
|
||||
}
|
||||
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
// Disabled form controls are always valid
|
||||
this.formControlController.setValidity(this.disabled);
|
||||
}
|
||||
|
||||
@watch(['checked', 'indeterminate'], { waitUntilFirstUpdate: true })
|
||||
handleStateChange() {
|
||||
this.input.checked = this.checked; // force a sync update
|
||||
this.input.indeterminate = this.indeterminate; // force a sync update
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
|
||||
/** Simulates a click on the checkbox. */
|
||||
@@ -88,51 +137,23 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC
|
||||
this.input.blur();
|
||||
}
|
||||
|
||||
/** Checks for validity but does not show the browser's validation message. */
|
||||
/** Checks for validity but does not show a validation message. Returns true when valid and false when invalid. */
|
||||
checkValidity() {
|
||||
return this.input.checkValidity();
|
||||
}
|
||||
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
/** Checks for validity and shows a validation message if the control is invalid. */
|
||||
reportValidity() {
|
||||
return this.input.reportValidity();
|
||||
}
|
||||
|
||||
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
|
||||
/**
|
||||
* Sets a custom validation message. The value provided will be shown to the user when the form is submitted. To clear
|
||||
* the custom validation message, call this method with an empty string.
|
||||
*/
|
||||
setCustomValidity(message: string) {
|
||||
this.input.setCustomValidity(message);
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
this.checked = !this.checked;
|
||||
this.indeterminate = false;
|
||||
this.emit('sl-change');
|
||||
}
|
||||
|
||||
handleBlur() {
|
||||
this.hasFocus = false;
|
||||
this.emit('sl-blur');
|
||||
}
|
||||
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
// Disabled form controls are always valid, so we need to recheck validity when the state changes
|
||||
this.input.disabled = this.disabled;
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
|
||||
handleFocus() {
|
||||
this.hasFocus = true;
|
||||
this.emit('sl-focus');
|
||||
}
|
||||
|
||||
@watch('checked', { waitUntilFirstUpdate: true })
|
||||
@watch('indeterminate', { waitUntilFirstUpdate: true })
|
||||
handleStateChange() {
|
||||
this.input.checked = this.checked; // force a sync update
|
||||
this.input.indeterminate = this.indeterminate; // force a sync update
|
||||
this.invalid = !this.input.checkValidity();
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -144,13 +165,17 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC
|
||||
'checkbox--checked': this.checked,
|
||||
'checkbox--disabled': this.disabled,
|
||||
'checkbox--focused': this.hasFocus,
|
||||
'checkbox--indeterminate': this.indeterminate
|
||||
'checkbox--indeterminate': this.indeterminate,
|
||||
'checkbox--small': this.size === 'small',
|
||||
'checkbox--medium': this.size === 'medium',
|
||||
'checkbox--large': this.size === 'large'
|
||||
})}
|
||||
>
|
||||
<input
|
||||
class="checkbox__input"
|
||||
type="checkbox"
|
||||
name=${ifDefined(this.name)}
|
||||
title=${this.title /* An empty title prevents browser validation tooltips from appearing on hover */}
|
||||
name=${this.name}
|
||||
value=${ifDefined(this.value)}
|
||||
.indeterminate=${live(this.indeterminate)}
|
||||
.checked=${live(this.checked)}
|
||||
@@ -158,20 +183,33 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC
|
||||
.required=${this.required}
|
||||
aria-checked=${this.checked ? 'true' : 'false'}
|
||||
@click=${this.handleClick}
|
||||
@input=${this.handleInput}
|
||||
@blur=${this.handleBlur}
|
||||
@focus=${this.handleFocus}
|
||||
/>
|
||||
|
||||
<span part="control" class="checkbox__control">
|
||||
${this.checked ? html` <sl-icon part="checked-icon" library="system" name="check"></sl-icon> ` : ''}
|
||||
<span
|
||||
part="control${this.checked ? ' control--checked' : ''}${this.indeterminate ? ' control--indeterminate' : ''}"
|
||||
class="checkbox__control"
|
||||
>
|
||||
${this.checked
|
||||
? html`
|
||||
<sl-icon part="checked-icon" class="checkbox__checked-icon" library="system" name="check"></sl-icon>
|
||||
`
|
||||
: ''}
|
||||
${!this.checked && this.indeterminate
|
||||
? html` <sl-icon part="indeterminate-icon" library="system" name="indeterminate"></sl-icon> `
|
||||
? html`
|
||||
<sl-icon
|
||||
part="indeterminate-icon"
|
||||
class="checkbox__indeterminate-icon"
|
||||
library="system"
|
||||
name="indeterminate"
|
||||
></sl-icon>
|
||||
`
|
||||
: ''}
|
||||
</span>
|
||||
|
||||
<span part="label" class="checkbox__label">
|
||||
<slot></slot>
|
||||
</span>
|
||||
<slot part="label" class="checkbox__label"></slot>
|
||||
</label>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -55,12 +55,12 @@ export default css`
|
||||
border: solid 2px white;
|
||||
margin-top: calc(var(--grid-handle-size) / -2);
|
||||
margin-left: calc(var(--grid-handle-size) / -2);
|
||||
transition: var(--sl-transition-fast) transform;
|
||||
transition: var(--sl-transition-fast) scale;
|
||||
}
|
||||
|
||||
.color-picker__grid-handle--dragging {
|
||||
cursor: none;
|
||||
transform: scale(1.5);
|
||||
scale: 1.5;
|
||||
}
|
||||
|
||||
.color-picker__grid-handle:focus-visible {
|
||||
|
||||
@@ -1,21 +1,289 @@
|
||||
import { expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
|
||||
import { aTimeout, expect, fixture, html, oneEvent } from '@open-wc/testing';
|
||||
import { clickOnElement } from '../../internal/test';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import { serialize } from '../../utilities/form';
|
||||
import sinon from 'sinon';
|
||||
import type SlColorPicker from './color-picker';
|
||||
|
||||
describe('<sl-color-picker>', () => {
|
||||
it('should emit change and show correct color when the value changes', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLElement>('[part~="trigger"]')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const color = 'rgb(255, 204, 0)';
|
||||
describe('when the value changes', () => {
|
||||
it('should not emit sl-change or sl-input when the value is changed programmatically', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
|
||||
const color = 'rgb(255, 204, 0)';
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.value = color;
|
||||
el.addEventListener('sl-change', () => expect.fail('sl-change should not be emitted'));
|
||||
el.addEventListener('sl-input', () => expect.fail('sl-change should not be emitted'));
|
||||
el.value = color;
|
||||
await el.updateComplete;
|
||||
});
|
||||
|
||||
await waitUntil(() => changeHandler.calledOnce);
|
||||
it('should emit sl-change and sl-input when the color grid selector is moved', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
|
||||
const grid = el.shadowRoot!.querySelector<HTMLElement>('[part~="grid"]')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(trigger.style.color).to.equal(color);
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
|
||||
await clickOnElement(trigger); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
await clickOnElement(grid); // click on the grid
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should emit sl-change and sl-input when the hue slider is moved', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
|
||||
const slider = el.shadowRoot!.querySelector<HTMLElement>('[part~="hue-slider"]')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
|
||||
await clickOnElement(trigger); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
await clickOnElement(slider); // click on the hue slider
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should emit sl-change and sl-input when the opacity slider is moved', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker opacity></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
|
||||
const slider = el.shadowRoot!.querySelector<HTMLElement>('[part~="opacity-slider"]')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
|
||||
await clickOnElement(trigger); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
await clickOnElement(slider); // click on the opacity slider
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should emit sl-change and sl-input when toggling the format', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker value="#fff"></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
|
||||
const formatButton = el.shadowRoot!.querySelector<HTMLElement>('[part~="format-button"]')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
|
||||
await clickOnElement(trigger); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
await clickOnElement(formatButton); // click on the format button
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.value).to.equal('rgb(255, 255, 255)');
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should render the correct swatches when passing a string of color values', async () => {
|
||||
const el = await fixture<SlColorPicker>(
|
||||
html` <sl-color-picker swatches="red; #008000; rgb(0,0,255);"></sl-color-picker> `
|
||||
);
|
||||
const swatches = [...el.shadowRoot!.querySelectorAll('[part~="swatch"] > div')];
|
||||
|
||||
expect(swatches.length).to.equal(3);
|
||||
expect(getComputedStyle(swatches[0]).backgroundColor).to.equal('rgb(255, 0, 0)');
|
||||
expect(getComputedStyle(swatches[1]).backgroundColor).to.equal('rgb(0, 128, 0)');
|
||||
expect(getComputedStyle(swatches[2]).backgroundColor).to.equal('rgb(0, 0, 255)');
|
||||
});
|
||||
|
||||
it('should render the correct swatches when passing an array of color values', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
|
||||
el.swatches = ['red', '#008000', 'rgb(0,0,255)'];
|
||||
await el.updateComplete;
|
||||
|
||||
const swatches = [...el.shadowRoot!.querySelectorAll('[part~="swatch"] > div')];
|
||||
|
||||
expect(swatches.length).to.equal(3);
|
||||
expect(getComputedStyle(swatches[0]).backgroundColor).to.equal('rgb(255, 0, 0)');
|
||||
expect(getComputedStyle(swatches[1]).backgroundColor).to.equal('rgb(0, 128, 0)');
|
||||
expect(getComputedStyle(swatches[2]).backgroundColor).to.equal('rgb(0, 0, 255)');
|
||||
});
|
||||
|
||||
it('should emit sl-change and sl-input when clicking on a swatch', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker swatches="red; green; blue;"></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
|
||||
const swatch = el.shadowRoot!.querySelector<HTMLElement>('[part~="swatch"]')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
|
||||
await clickOnElement(trigger); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
await clickOnElement(swatch); // click on the swatch
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should emit sl-change and sl-input when selecting a color with the keyboard', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
|
||||
const gridHandle = el.shadowRoot!.querySelector<HTMLElement>('[part~="grid-handle"]')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
|
||||
await clickOnElement(trigger); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
gridHandle.focus();
|
||||
await sendKeys({ press: 'ArrowRight' }); // move the grid handle
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should emit sl-change and sl-input when selecting a color with the keyboard', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
|
||||
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="grid-handle"]')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
|
||||
await clickOnElement(trigger); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
handle.focus();
|
||||
await sendKeys({ press: 'ArrowRight' }); // move the handle
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should emit sl-change and sl-input when selecting hue with the keyboard', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
|
||||
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="hue-slider"] > span')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
|
||||
await clickOnElement(trigger); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
handle.focus();
|
||||
await sendKeys({ press: 'ArrowRight' }); // move the handle
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should emit sl-change and sl-input when selecting opacity with the keyboard', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker opacity></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
|
||||
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="opacity-slider"] > span')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
|
||||
await clickOnElement(trigger); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
handle.focus();
|
||||
await sendKeys({ press: 'ArrowRight' }); // move the handle
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should emit sl-change and sl-input when entering a value in the color input and pressing enter', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker opacity></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
|
||||
const input = el.shadowRoot!.querySelector<HTMLElement>('[part~="input"]')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
|
||||
await clickOnElement(trigger); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
input.focus(); // focus the input
|
||||
await el.updateComplete;
|
||||
await sendKeys({ type: 'fc0' }); // type in a color
|
||||
await sendKeys({ press: 'Enter' }); // press enter
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should emit sl-change and sl-input when entering a value in the color input and blurring the field', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker opacity></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
|
||||
const input = el.shadowRoot!.querySelector<HTMLElement>('[part~="input"]')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
|
||||
await clickOnElement(trigger); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
input.focus(); // focus the input
|
||||
await el.updateComplete;
|
||||
await sendKeys({ type: 'fc0' }); // type in a color
|
||||
input.blur(); // commit changes by blurring the field
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should render the correct format when selecting a swatch of a different format', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker format="rgb"></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
|
||||
const changeHandler = sinon.spy();
|
||||
const inputHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
|
||||
el.swatches = ['#fff'];
|
||||
await el.updateComplete;
|
||||
const swatch = el.shadowRoot!.querySelector<HTMLElement>('[part~="swatch"]')!;
|
||||
|
||||
await clickOnElement(trigger); // open the dropdown
|
||||
await aTimeout(200); // wait for the dropdown to open
|
||||
await clickOnElement(swatch); // click on the swatch
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.value).to.equal('rgb(255, 255, 255)');
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
});
|
||||
});
|
||||
|
||||
it('should render in a dropdown', async () => {
|
||||
@@ -48,19 +316,56 @@ describe('<sl-color-picker>', () => {
|
||||
|
||||
it('should display a color with opacity when an initial value with opacity is provided', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker opacity value="#ff000050"></sl-color-picker> `);
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]');
|
||||
const trigger = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="trigger"]')!;
|
||||
const previewButton = el.shadowRoot!.querySelector<HTMLButtonElement>('[part~="preview"]');
|
||||
const previewColor = getComputedStyle(previewButton!).getPropertyValue('--preview-color');
|
||||
|
||||
expect(trigger!.style.color).to.equal('rgba(255, 0, 0, 0.314)');
|
||||
expect(previewColor.startsWith('hsla(0deg, 100%, 50%, 0.31')).to.be.true;
|
||||
expect(trigger.style.color).to.equal('rgba(255, 0, 0, 0.314)');
|
||||
expect(previewColor).to.equal('#ff000050');
|
||||
});
|
||||
|
||||
describe('when submitting a form', () => {
|
||||
it('should serialize its name and value with FormData', async () => {
|
||||
const form = await fixture<HTMLFormElement>(html`
|
||||
<form>
|
||||
<sl-color-picker name="a" value="#ffcc00"></sl-color-picker>
|
||||
</form>
|
||||
`);
|
||||
const formData = new FormData(form);
|
||||
expect(formData.get('a')).to.equal('#ffcc00');
|
||||
});
|
||||
|
||||
it('should serialize its name and value with JSON', async () => {
|
||||
const form = await fixture<HTMLFormElement>(html`
|
||||
<form>
|
||||
<sl-color-picker name="a" value="#ffcc00"></sl-color-picker>
|
||||
</form>
|
||||
`);
|
||||
const json = serialize(form);
|
||||
expect(json.a).to.equal('#ffcc00');
|
||||
});
|
||||
|
||||
it('should be present in form data when using the form attribute and located outside of a <form>', async () => {
|
||||
const el = await fixture<HTMLFormElement>(html`
|
||||
<div>
|
||||
<form id="f">
|
||||
<sl-button type="submit">Submit</sl-button>
|
||||
</form>
|
||||
<sl-color-picker form="f" name="a" value="#ffcc00"></sl-color-picker>
|
||||
</div>
|
||||
`);
|
||||
const form = el.querySelector('form')!;
|
||||
const formData = new FormData(form);
|
||||
|
||||
expect(formData.get('a')).to.equal('#ffcc00');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when resetting a form', () => {
|
||||
it('should reset the element to its initial value', async () => {
|
||||
const form = await fixture<HTMLFormElement>(html`
|
||||
<form>
|
||||
<sl-color-picker name="a" value="#FFFFFF"></sl-color-picker>
|
||||
<sl-color-picker name="a" value="#ffffff"></sl-color-picker>
|
||||
<sl-button type="reset">Reset</sl-button>
|
||||
</form>
|
||||
`);
|
||||
@@ -74,7 +379,7 @@ describe('<sl-color-picker>', () => {
|
||||
await oneEvent(form, 'reset');
|
||||
await colorPicker.updateComplete;
|
||||
|
||||
expect(colorPicker.value).to.equal('#FFFFFF');
|
||||
expect(colorPicker.value).to.equal('#ffffff');
|
||||
|
||||
colorPicker.defaultValue = '';
|
||||
|
||||
@@ -85,4 +390,62 @@ describe('<sl-color-picker>', () => {
|
||||
expect(colorPicker.value).to.equal('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using constraint validation', () => {
|
||||
it('should be valid by default', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-color-picker></sl-color-picker> `);
|
||||
expect(el.checkValidity()).to.be.true;
|
||||
});
|
||||
|
||||
it('should be invalid when required and empty', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-input required></sl-input> `);
|
||||
expect(el.checkValidity()).to.be.false;
|
||||
});
|
||||
|
||||
it('should be invalid when required and disabled is removed', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-input disabled required></sl-input> `);
|
||||
el.disabled = false;
|
||||
await el.updateComplete;
|
||||
expect(el.checkValidity()).to.be.false;
|
||||
});
|
||||
|
||||
it('should receive the correct validation attributes ("states") when valid', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-input required value="a"></sl-input> `);
|
||||
|
||||
expect(el.checkValidity()).to.be.true;
|
||||
expect(el.hasAttribute('data-required')).to.be.true;
|
||||
expect(el.hasAttribute('data-optional')).to.be.false;
|
||||
expect(el.hasAttribute('data-invalid')).to.be.false;
|
||||
expect(el.hasAttribute('data-valid')).to.be.true;
|
||||
expect(el.hasAttribute('data-user-invalid')).to.be.false;
|
||||
expect(el.hasAttribute('data-user-valid')).to.be.false;
|
||||
|
||||
el.focus();
|
||||
await sendKeys({ press: 'b' });
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.checkValidity()).to.be.true;
|
||||
expect(el.hasAttribute('data-user-invalid')).to.be.false;
|
||||
expect(el.hasAttribute('data-user-valid')).to.be.true;
|
||||
});
|
||||
|
||||
it('should receive the correct validation attributes ("states") when invalid', async () => {
|
||||
const el = await fixture<SlColorPicker>(html` <sl-input required></sl-input> `);
|
||||
|
||||
expect(el.hasAttribute('data-required')).to.be.true;
|
||||
expect(el.hasAttribute('data-optional')).to.be.false;
|
||||
expect(el.hasAttribute('data-invalid')).to.be.true;
|
||||
expect(el.hasAttribute('data-valid')).to.be.false;
|
||||
expect(el.hasAttribute('data-user-invalid')).to.be.false;
|
||||
expect(el.hasAttribute('data-user-valid')).to.be.false;
|
||||
|
||||
el.focus();
|
||||
await sendKeys({ press: 'a' });
|
||||
await sendKeys({ press: 'Backspace' });
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.hasAttribute('data-user-invalid')).to.be.true;
|
||||
expect(el.hasAttribute('data-user-valid')).to.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
import Color from 'color';
|
||||
import { html } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { live } from 'lit/directives/live.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { defaultValue } from '../../internal/default-value';
|
||||
import { drag } from '../../internal/drag';
|
||||
import { FormSubmitController } from '../../internal/form';
|
||||
import { clamp } from '../../internal/math';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import '../button-group/button-group';
|
||||
import '../button/button';
|
||||
import '../dropdown/dropdown';
|
||||
import '../icon/icon';
|
||||
import '../input/input';
|
||||
import '../visually-hidden/visually-hidden';
|
||||
import { clamp } from '../../internal/math';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { defaultValue } from '../../internal/default-value';
|
||||
import { drag } from '../../internal/drag';
|
||||
import { FormControlController } from '../../internal/form';
|
||||
import { html } from 'lit';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { TinyColor } from '@ctrl/tinycolor';
|
||||
import { watch } from '../../internal/watch';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import styles from './color-picker.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
import type { ShoelaceFormControl } from '../../internal/shoelace-element';
|
||||
import type SlDropdown from '../dropdown/dropdown';
|
||||
import type SlInput from '../input/input';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
const hasEyeDropper = 'EyeDropper' in window;
|
||||
|
||||
@@ -38,9 +37,9 @@ declare const EyeDropper: EyeDropperConstructor;
|
||||
|
||||
/**
|
||||
* @summary Color pickers allow the user to select a color.
|
||||
*
|
||||
* @since 2.0
|
||||
* @documentation https://shoelace.style/components/color-picker
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @dependency sl-button
|
||||
* @dependency sl-button-group
|
||||
@@ -48,34 +47,37 @@ declare const EyeDropper: EyeDropperConstructor;
|
||||
* @dependency sl-input
|
||||
* @dependency sl-visually-hidden
|
||||
*
|
||||
* @slot label - The color picker's label. Alternatively, you can use the `label` attribute.
|
||||
* @slot label - The color picker's form label. Alternatively, you can use the `label` attribute.
|
||||
*
|
||||
* @event sl-change Emitted when the color picker's value changes.
|
||||
* @event sl-input Emitted when the color picker receives input.
|
||||
*
|
||||
* @csspart base - The component's internal wrapper.
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart trigger - The color picker's dropdown trigger.
|
||||
* @csspart swatches - The container that holds swatches.
|
||||
* @csspart swatches - The container that holds the swatches.
|
||||
* @csspart swatch - Each individual swatch.
|
||||
* @csspart grid - The color grid.
|
||||
* @csspart grid-handle - The color grid's handle.
|
||||
* @csspart hue-slider - The hue slider.
|
||||
* @csspart opacity-slider - The opacity slider.
|
||||
* @csspart slider - Hue and opacity sliders.
|
||||
* @csspart slider-handle - Hue and opacity slider handles.
|
||||
* @csspart hue-slider - The hue slider.
|
||||
* @csspart hue-slider-handle - The hue slider's handle.
|
||||
* @csspart opacity-slider - The opacity slider.
|
||||
* @csspart opacity-slider-handle - The opacity slider's handle.
|
||||
* @csspart preview - The preview color.
|
||||
* @csspart input - The text input.
|
||||
* @csspart eye-dropper-button - The eye dropper button.
|
||||
* @csspart eye-dropper-button__button - The eye dropper button's `button` part.
|
||||
* @csspart eye-dropper-button__prefix - The eye dropper button's `prefix` part.
|
||||
* @csspart eye-dropper-button__label - The eye dropper button's `label` part.
|
||||
* @csspart eye-dropper-button__button-suffix - The eye dropper button's `suffix` part.
|
||||
* @csspart eye-dropper-button__caret - The eye dropper button's `caret` part.
|
||||
* @csspart eye-dropper-button__base - The eye dropper button's exported `button` part.
|
||||
* @csspart eye-dropper-button__prefix - The eye dropper button's exported `prefix` part.
|
||||
* @csspart eye-dropper-button__label - The eye dropper button's exported `label` part.
|
||||
* @csspart eye-dropper-button__suffix - The eye dropper button's exported `suffix` part.
|
||||
* @csspart eye-dropper-button__caret - The eye dropper button's exported `caret` part.
|
||||
* @csspart format-button - The format button.
|
||||
* @csspart format-button__button - The format button's `button` part.
|
||||
* @csspart format-button__prefix - The format button's `prefix` part.
|
||||
* @csspart format-button__label - The format button's `label` part.
|
||||
* @csspart format-button__button-suffix - The format button's `suffix` part.
|
||||
* @csspart format-button__caret - The format button's `caret` part.
|
||||
* @csspart format-button__base - The format button's exported `button` part.
|
||||
* @csspart format-button__prefix - The format button's exported `prefix` part.
|
||||
* @csspart format-button__label - The format button's exported `label` part.
|
||||
* @csspart format-button__suffix - The format button's exported `suffix` part.
|
||||
* @csspart format-button__caret - The format button's exported `caret` part.
|
||||
*
|
||||
* @cssproperty --grid-width - The width of the color grid.
|
||||
* @cssproperty --grid-height - The height of the color grid.
|
||||
@@ -88,30 +90,30 @@ declare const EyeDropper: EyeDropperConstructor;
|
||||
export default class SlColorPicker extends ShoelaceElement implements ShoelaceFormControl {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
private readonly formControlController = new FormControlController(this);
|
||||
private isSafeValue = false;
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
@query('[part~="input"]') input: SlInput;
|
||||
@query('[part~="preview"]') previewButton: HTMLButtonElement;
|
||||
@query('.color-dropdown') dropdown: SlDropdown;
|
||||
|
||||
// @ts-expect-error -- Controller is currently unused
|
||||
private readonly formSubmitController = new FormSubmitController(this);
|
||||
private isSafeValue = false;
|
||||
private lastValueEmitted: string;
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
@state() private isDraggingGridHandle = false;
|
||||
@state() private isEmpty = false;
|
||||
@state() private inputValue = '';
|
||||
@state() private hue = 0;
|
||||
@state() private saturation = 100;
|
||||
@state() private lightness = 100;
|
||||
@state() private brightness = 100;
|
||||
@state() private alpha = 100;
|
||||
@state() invalid = false;
|
||||
|
||||
/** The current color. */
|
||||
/**
|
||||
* The current value of the color picker. The value's format will vary based the `format` attribute. To get the value
|
||||
* in a specific format, use the `getFormattedValue()` method. The value is submitted as a name/value pair with form
|
||||
* data.
|
||||
*/
|
||||
@property() value = '';
|
||||
|
||||
/** Gets or sets the default value used to reset this element. The initial value corresponds to the one originally specified in the HTML that created this element. */
|
||||
/** The default value of the form control. Primarily used for resetting the form control. */
|
||||
@defaultValue() defaultValue = '';
|
||||
|
||||
/**
|
||||
@@ -121,22 +123,21 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
@property() label = '';
|
||||
|
||||
/**
|
||||
* The format to use for the display value. If opacity is enabled, these will translate to HEXA, RGBA, and HSLA
|
||||
* respectively. The color picker will always accept user input in any format (including CSS color names) and convert
|
||||
* it to the desired format.
|
||||
* The format to use. If opacity is enabled, these will translate to HEXA, RGBA, HSLA, and HSVA respectively. The color
|
||||
* picker will accept user input in any format (including CSS color names) and convert it to the desired format.
|
||||
*/
|
||||
@property() format: 'hex' | 'rgb' | 'hsl' = 'hex';
|
||||
@property() format: 'hex' | 'rgb' | 'hsl' | 'hsv' = 'hex';
|
||||
|
||||
/** Renders the color picker inline rather than inside a dropdown. */
|
||||
/** Renders the color picker inline rather than in a dropdown. */
|
||||
@property({ type: Boolean, reflect: true }) inline = false;
|
||||
|
||||
/** Determines the size of the color picker's trigger. This has no effect on inline color pickers. */
|
||||
@property() size: 'small' | 'medium' | 'large' = 'medium';
|
||||
|
||||
/** Removes the format toggle. */
|
||||
/** Removes the button that lets users toggle between format. */
|
||||
@property({ attribute: 'no-format-toggle', type: Boolean }) noFormatToggle = false;
|
||||
|
||||
/** The input's name attribute. */
|
||||
/** The name of the form control, submitted as a name/value pair with form data. */
|
||||
@property() name = '';
|
||||
|
||||
/** Disables the color picker. */
|
||||
@@ -144,114 +145,31 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
|
||||
/**
|
||||
* Enable this option to prevent the panel from being clipped when the component is placed inside a container with
|
||||
* `overflow: auto|scroll`.
|
||||
* `overflow: auto|scroll`. Hoisting uses a fixed positioning strategy that works in many, but not all, scenarios.
|
||||
*/
|
||||
@property({ type: Boolean }) hoist = false;
|
||||
|
||||
/** Whether to show the opacity slider. */
|
||||
/** Shows the opacity slider. Enabling this will cause the formatted value to be HEXA, RGBA, or HSLA. */
|
||||
@property({ type: Boolean }) opacity = false;
|
||||
|
||||
/** By default, the value will be set in lowercase. Set this to true to set it in uppercase instead. */
|
||||
/** By default, values are lowercase. With this attribute, values will be uppercase instead. */
|
||||
@property({ type: Boolean }) uppercase = false;
|
||||
|
||||
/**
|
||||
* An array of predefined color swatches to display. Can include any format the color picker can parse, including
|
||||
* HEX(A), RGB(A), HSL(A), and CSS color names.
|
||||
* One or more predefined color swatches to display as presets in the color picker. Can include any format the color
|
||||
* picker can parse, including HEX(A), RGB(A), HSL(A), HSV(A), and CSS color names. Each color must be separated by a
|
||||
* semicolon (`;`). Alternatively, you can pass an array of color values to this property using JavaScript.
|
||||
*/
|
||||
@property({ attribute: false }) swatches: string[] = [
|
||||
'#d0021b',
|
||||
'#f5a623',
|
||||
'#f8e71c',
|
||||
'#8b572a',
|
||||
'#7ed321',
|
||||
'#417505',
|
||||
'#bd10e0',
|
||||
'#9013fe',
|
||||
'#4a90e2',
|
||||
'#50e3c2',
|
||||
'#b8e986',
|
||||
'#000',
|
||||
'#444',
|
||||
'#888',
|
||||
'#ccc',
|
||||
'#fff'
|
||||
];
|
||||
@property() swatches: string | string[] = '';
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
/**
|
||||
* By default, form controls are associated with the nearest containing `<form>` element. This attribute allows you
|
||||
* to place the form control outside of a form and associate it with the form that has this `id`. The form must be in
|
||||
* the same document or shadow root for this to work.
|
||||
*/
|
||||
@property({ reflect: true }) form = '';
|
||||
|
||||
if (this.value) {
|
||||
this.setColor(this.value);
|
||||
this.inputValue = this.value;
|
||||
this.lastValueEmitted = this.value;
|
||||
this.syncValues();
|
||||
} else {
|
||||
this.isEmpty = true;
|
||||
this.inputValue = '';
|
||||
this.lastValueEmitted = '';
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the current value as a string in the specified format. */
|
||||
getFormattedValue(format: 'hex' | 'hexa' | 'rgb' | 'rgba' | 'hsl' | 'hsla' = 'hex') {
|
||||
const currentColor = this.parseColor(
|
||||
`hsla(${this.hue}, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})`
|
||||
);
|
||||
|
||||
if (currentColor === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
switch (format) {
|
||||
case 'hex':
|
||||
return currentColor.hex;
|
||||
case 'hexa':
|
||||
return currentColor.hexa;
|
||||
case 'rgb':
|
||||
return currentColor.rgb.string;
|
||||
case 'rgba':
|
||||
return currentColor.rgba.string;
|
||||
case 'hsl':
|
||||
return currentColor.hsl.string;
|
||||
case 'hsla':
|
||||
return currentColor.hsla.string;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
getBrightness(lightness: number) {
|
||||
return clamp(-1 * ((200 * lightness) / (this.saturation - 200)), 0, 100);
|
||||
}
|
||||
|
||||
getLightness(brightness: number) {
|
||||
return clamp(((((200 - this.saturation) * brightness) / 100) * 5) / 10, 0, 100);
|
||||
}
|
||||
|
||||
/** Checks for validity but does not show the browser's validation message. */
|
||||
checkValidity() {
|
||||
return this.input.checkValidity();
|
||||
}
|
||||
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
reportValidity() {
|
||||
if (!this.inline && this.input.invalid) {
|
||||
// If the input is inline and invalid, show the dropdown so the browser can focus on it
|
||||
this.dropdown.show();
|
||||
this.addEventListener('sl-after-show', () => this.input.reportValidity(), { once: true });
|
||||
return this.checkValidity();
|
||||
}
|
||||
|
||||
return this.input.reportValidity();
|
||||
}
|
||||
|
||||
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
|
||||
setCustomValidity(message: string) {
|
||||
this.input.setCustomValidity(message);
|
||||
this.invalid = this.input.invalid;
|
||||
}
|
||||
|
||||
handleCopy() {
|
||||
private handleCopy() {
|
||||
this.input.select();
|
||||
document.execCommand('copy');
|
||||
this.previewButton.focus();
|
||||
@@ -263,16 +181,20 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
});
|
||||
}
|
||||
|
||||
handleFormatToggle() {
|
||||
const formats = ['hex', 'rgb', 'hsl'];
|
||||
private handleFormatToggle() {
|
||||
const formats = ['hex', 'rgb', 'hsl', 'hsv'];
|
||||
const nextIndex = (formats.indexOf(this.format) + 1) % formats.length;
|
||||
this.format = formats[nextIndex] as 'hex' | 'rgb' | 'hsl';
|
||||
this.format = formats[nextIndex] as 'hex' | 'rgb' | 'hsl' | 'hsv';
|
||||
this.setColor(this.value);
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
|
||||
handleAlphaDrag(event: PointerEvent) {
|
||||
private handleAlphaDrag(event: PointerEvent) {
|
||||
const container = this.shadowRoot!.querySelector<HTMLElement>('.color-picker__slider.color-picker__alpha')!;
|
||||
const handle = container.querySelector<HTMLElement>('.color-picker__slider-handle')!;
|
||||
const { width } = container.getBoundingClientRect();
|
||||
let oldValue = this.value;
|
||||
|
||||
handle.focus();
|
||||
event.preventDefault();
|
||||
@@ -281,15 +203,22 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
onMove: x => {
|
||||
this.alpha = clamp((x / width) * 100, 0, 100);
|
||||
this.syncValues();
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
oldValue = this.value;
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
},
|
||||
initialEvent: event
|
||||
});
|
||||
}
|
||||
|
||||
handleHueDrag(event: PointerEvent) {
|
||||
private handleHueDrag(event: PointerEvent) {
|
||||
const container = this.shadowRoot!.querySelector<HTMLElement>('.color-picker__slider.color-picker__hue')!;
|
||||
const handle = container.querySelector<HTMLElement>('.color-picker__slider-handle')!;
|
||||
const { width } = container.getBoundingClientRect();
|
||||
let oldValue = this.value;
|
||||
|
||||
handle.focus();
|
||||
event.preventDefault();
|
||||
@@ -298,15 +227,22 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
onMove: x => {
|
||||
this.hue = clamp((x / width) * 360, 0, 360);
|
||||
this.syncValues();
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
oldValue = this.value;
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
},
|
||||
initialEvent: event
|
||||
});
|
||||
}
|
||||
|
||||
handleGridDrag(event: PointerEvent) {
|
||||
private handleGridDrag(event: PointerEvent) {
|
||||
const grid = this.shadowRoot!.querySelector<HTMLElement>('.color-picker__grid')!;
|
||||
const handle = grid.querySelector<HTMLElement>('.color-picker__grid-handle')!;
|
||||
const { width, height } = grid.getBoundingClientRect();
|
||||
let oldValue = this.value;
|
||||
|
||||
handle.focus();
|
||||
event.preventDefault();
|
||||
@@ -317,16 +253,22 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
onMove: (x, y) => {
|
||||
this.saturation = clamp((x / width) * 100, 0, 100);
|
||||
this.brightness = clamp(100 - (y / height) * 100, 0, 100);
|
||||
this.lightness = this.getLightness(this.brightness);
|
||||
this.syncValues();
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
oldValue = this.value;
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
},
|
||||
onStop: () => (this.isDraggingGridHandle = false),
|
||||
initialEvent: event
|
||||
});
|
||||
}
|
||||
|
||||
handleAlphaKeyDown(event: KeyboardEvent) {
|
||||
private handleAlphaKeyDown(event: KeyboardEvent) {
|
||||
const increment = event.shiftKey ? 10 : 1;
|
||||
const oldValue = this.value;
|
||||
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
@@ -351,10 +293,16 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
this.alpha = 100;
|
||||
this.syncValues();
|
||||
}
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
}
|
||||
|
||||
handleHueKeyDown(event: KeyboardEvent) {
|
||||
private handleHueKeyDown(event: KeyboardEvent) {
|
||||
const increment = event.shiftKey ? 10 : 1;
|
||||
const oldValue = this.value;
|
||||
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
@@ -379,42 +327,53 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
this.hue = 360;
|
||||
this.syncValues();
|
||||
}
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
}
|
||||
|
||||
handleGridKeyDown(event: KeyboardEvent) {
|
||||
private handleGridKeyDown(event: KeyboardEvent) {
|
||||
const increment = event.shiftKey ? 10 : 1;
|
||||
const oldValue = this.value;
|
||||
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
this.saturation = clamp(this.saturation - increment, 0, 100);
|
||||
this.lightness = this.getLightness(this.brightness);
|
||||
this.syncValues();
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowRight') {
|
||||
event.preventDefault();
|
||||
this.saturation = clamp(this.saturation + increment, 0, 100);
|
||||
this.lightness = this.getLightness(this.brightness);
|
||||
this.syncValues();
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
this.brightness = clamp(this.brightness + increment, 0, 100);
|
||||
this.lightness = this.getLightness(this.brightness);
|
||||
this.syncValues();
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
this.brightness = clamp(this.brightness - increment, 0, 100);
|
||||
this.lightness = this.getLightness(this.brightness);
|
||||
this.syncValues();
|
||||
}
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
}
|
||||
|
||||
handleInputChange(event: CustomEvent) {
|
||||
private handleInputChange(event: CustomEvent) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const oldValue = this.value;
|
||||
|
||||
// Prevent the <sl-input>'s sl-change event from bubbling up
|
||||
event.stopPropagation();
|
||||
|
||||
if (this.input.value) {
|
||||
this.setColor(target.value);
|
||||
@@ -423,14 +382,30 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
this.value = '';
|
||||
}
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
}
|
||||
|
||||
private handleInputInput(event: CustomEvent) {
|
||||
// Prevent the <sl-input>'s sl-input event from bubbling up
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
handleInputKeyDown(event: KeyboardEvent) {
|
||||
private handleInputKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter') {
|
||||
const oldValue = this.value;
|
||||
|
||||
if (this.input.value) {
|
||||
this.setColor(this.input.value);
|
||||
this.input.value = this.value;
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
|
||||
setTimeout(() => this.input.select());
|
||||
} else {
|
||||
this.hue = 0;
|
||||
@@ -438,90 +413,37 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
}
|
||||
}
|
||||
|
||||
normalizeColorString(colorString: string) {
|
||||
//
|
||||
// The color module we're using doesn't parse % values for the alpha channel in RGBA and HSLA. It also doesn't parse
|
||||
// hex colors when the # is missing. This pre-parser tries to normalize these edge cases to provide a better
|
||||
// experience for users who type in color values.
|
||||
//
|
||||
if (/rgba?/i.test(colorString)) {
|
||||
const rgba = colorString
|
||||
.replace(/[^\d.%]/g, ' ')
|
||||
.split(' ')
|
||||
.map(val => val.trim())
|
||||
.filter(val => val.length);
|
||||
|
||||
if (rgba.length < 4) {
|
||||
rgba[3] = '1';
|
||||
}
|
||||
|
||||
if (rgba[3].indexOf('%') > -1) {
|
||||
rgba[3] = (parseFloat(rgba[3].replace(/%/g, '')) / 100).toString();
|
||||
}
|
||||
|
||||
return `rgba(${rgba[0]}, ${rgba[1]}, ${rgba[2]}, ${rgba[3]})`;
|
||||
}
|
||||
|
||||
if (/hsla?/i.test(colorString)) {
|
||||
const hsla = colorString
|
||||
.replace(/[^\d.%]/g, ' ')
|
||||
.split(' ')
|
||||
.map(val => val.trim())
|
||||
.filter(val => val.length);
|
||||
|
||||
if (hsla.length < 4) {
|
||||
hsla[3] = '1';
|
||||
}
|
||||
|
||||
if (hsla[3].indexOf('%') > -1) {
|
||||
hsla[3] = (parseFloat(hsla[3].replace(/%/g, '')) / 100).toString();
|
||||
}
|
||||
|
||||
return `hsla(${hsla[0]}, ${hsla[1]}, ${hsla[2]}, ${hsla[3]})`;
|
||||
}
|
||||
|
||||
if (/^[0-9a-f]+$/i.test(colorString)) {
|
||||
return `#${colorString}`;
|
||||
}
|
||||
|
||||
return colorString;
|
||||
private handleTouchMove(event: TouchEvent) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
parseColor(colorString: string) {
|
||||
let parsed: Color;
|
||||
|
||||
// The color module has a weak parser, so we normalize certain things to make the user experience better
|
||||
colorString = this.normalizeColorString(colorString);
|
||||
|
||||
try {
|
||||
parsed = Color(colorString);
|
||||
} catch {
|
||||
private parseColor(colorString: string) {
|
||||
const color = new TinyColor(colorString);
|
||||
if (!color.isValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hslColor = parsed.hsl();
|
||||
|
||||
const hslColor = color.toHsl();
|
||||
// Adjust saturation and lightness from 0-1 to 0-100
|
||||
const hsl = {
|
||||
h: hslColor.hue(),
|
||||
s: hslColor.saturationl(),
|
||||
l: hslColor.lightness(),
|
||||
a: hslColor.alpha()
|
||||
h: hslColor.h,
|
||||
s: hslColor.s * 100,
|
||||
l: hslColor.l * 100,
|
||||
a: hslColor.a
|
||||
};
|
||||
|
||||
const rgbColor = parsed.rgb();
|
||||
const rgb = color.toRgb();
|
||||
|
||||
const rgb = {
|
||||
r: rgbColor.red(),
|
||||
g: rgbColor.green(),
|
||||
b: rgbColor.blue(),
|
||||
a: rgbColor.alpha()
|
||||
};
|
||||
const hex = color.toHexString();
|
||||
const hexa = color.toHex8String();
|
||||
|
||||
const hex = {
|
||||
r: toHex(rgb.r),
|
||||
g: toHex(rgb.g),
|
||||
b: toHex(rgb.b),
|
||||
a: toHex(rgb.a * 255)
|
||||
const hsvColor = color.toHsv();
|
||||
// Adjust saturation and value from 0-1 to 0-100
|
||||
const hsv = {
|
||||
h: hsvColor.h,
|
||||
s: hsvColor.s * 100,
|
||||
v: hsvColor.v * 100,
|
||||
a: hsvColor.a
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -540,6 +462,21 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
`hsla(${Math.round(hsl.h)}, ${Math.round(hsl.s)}%, ${Math.round(hsl.l)}%, ${hsl.a.toFixed(2).toString()})`
|
||||
)
|
||||
},
|
||||
hsv: {
|
||||
h: hsv.h,
|
||||
s: hsv.s,
|
||||
v: hsv.v,
|
||||
string: this.setLetterCase(`hsv(${Math.round(hsv.h)}, ${Math.round(hsv.s)}%, ${Math.round(hsv.v)}%)`)
|
||||
},
|
||||
hsva: {
|
||||
h: hsv.h,
|
||||
s: hsv.s,
|
||||
v: hsv.v,
|
||||
a: hsv.a,
|
||||
string: this.setLetterCase(
|
||||
`hsva(${Math.round(hsv.h)}, ${Math.round(hsv.s)}%, ${Math.round(hsv.v)}%, ${hsv.a.toFixed(2).toString()})`
|
||||
)
|
||||
},
|
||||
rgb: {
|
||||
r: rgb.r,
|
||||
g: rgb.g,
|
||||
@@ -555,39 +492,38 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
`rgba(${Math.round(rgb.r)}, ${Math.round(rgb.g)}, ${Math.round(rgb.b)}, ${rgb.a.toFixed(2).toString()})`
|
||||
)
|
||||
},
|
||||
hex: this.setLetterCase(`#${hex.r}${hex.g}${hex.b}`),
|
||||
hexa: this.setLetterCase(`#${hex.r}${hex.g}${hex.b}${hex.a}`)
|
||||
hex: this.setLetterCase(hex),
|
||||
hexa: this.setLetterCase(hexa)
|
||||
};
|
||||
}
|
||||
|
||||
setColor(colorString: string) {
|
||||
private setColor(colorString: string) {
|
||||
const newColor = this.parseColor(colorString);
|
||||
|
||||
if (newColor === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.hue = newColor.hsla.h;
|
||||
this.saturation = newColor.hsla.s;
|
||||
this.lightness = newColor.hsla.l;
|
||||
this.brightness = this.getBrightness(newColor.hsla.l);
|
||||
this.alpha = this.opacity ? newColor.hsla.a * 100 : 100;
|
||||
this.hue = newColor.hsva.h;
|
||||
this.saturation = newColor.hsva.s;
|
||||
this.brightness = newColor.hsva.v;
|
||||
this.alpha = this.opacity ? newColor.hsva.a * 100 : 100;
|
||||
|
||||
this.syncValues();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
setLetterCase(string: string) {
|
||||
private setLetterCase(string: string) {
|
||||
if (typeof string !== 'string') {
|
||||
return '';
|
||||
}
|
||||
return this.uppercase ? string.toUpperCase() : string.toLowerCase();
|
||||
}
|
||||
|
||||
async syncValues() {
|
||||
private async syncValues() {
|
||||
const currentColor = this.parseColor(
|
||||
`hsla(${this.hue}, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})`
|
||||
`hsva(${this.hue}, ${this.saturation}%, ${this.brightness}%, ${this.alpha / 100})`
|
||||
);
|
||||
|
||||
if (currentColor === null) {
|
||||
@@ -599,6 +535,8 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
this.inputValue = this.opacity ? currentColor.hsla.string : currentColor.hsl.string;
|
||||
} else if (this.format === 'rgb') {
|
||||
this.inputValue = this.opacity ? currentColor.rgba.string : currentColor.rgb.string;
|
||||
} else if (this.format === 'hsv') {
|
||||
this.inputValue = this.opacity ? currentColor.hsva.string : currentColor.hsv.string;
|
||||
} else {
|
||||
this.inputValue = this.opacity ? currentColor.hexa : currentColor.hex;
|
||||
}
|
||||
@@ -612,11 +550,11 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
this.isSafeValue = false;
|
||||
}
|
||||
|
||||
handleAfterHide() {
|
||||
private handleAfterHide() {
|
||||
this.previewButton.classList.remove('color-picker__preview-color--copied');
|
||||
}
|
||||
|
||||
handleEyeDropper() {
|
||||
private handleEyeDropper() {
|
||||
if (!hasEyeDropper) {
|
||||
return;
|
||||
}
|
||||
@@ -631,6 +569,29 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
});
|
||||
}
|
||||
|
||||
private selectSwatch(color: string) {
|
||||
const oldValue = this.value;
|
||||
|
||||
if (!this.disabled) {
|
||||
this.setColor(color);
|
||||
|
||||
if (this.value !== oldValue) {
|
||||
this.emit('sl-change');
|
||||
this.emit('sl-input');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Generates a hex string from HSV values. Hue must be 0-360. All other arguments must be 0-100. */
|
||||
private getHexString(hue: number, saturation: number, brightness: number, alpha = 100) {
|
||||
const color = new TinyColor(`hsva(${hue}, ${saturation}, ${brightness}, ${alpha / 100})`);
|
||||
if (!color.isValid) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return color.toHex8String();
|
||||
}
|
||||
|
||||
@watch('format', { waitUntilFirstUpdate: true })
|
||||
handleFormatChange() {
|
||||
this.syncValues();
|
||||
@@ -647,35 +608,88 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
|
||||
if (!newValue) {
|
||||
this.hue = 0;
|
||||
this.saturation = 100;
|
||||
this.saturation = 0;
|
||||
this.brightness = 100;
|
||||
this.lightness = this.getLightness(this.brightness);
|
||||
this.alpha = 100;
|
||||
}
|
||||
if (!this.isSafeValue && oldValue !== undefined) {
|
||||
|
||||
if (!this.isSafeValue) {
|
||||
const newColor = this.parseColor(newValue);
|
||||
|
||||
if (newColor !== null) {
|
||||
this.inputValue = this.value;
|
||||
this.hue = newColor.hsla.h;
|
||||
this.saturation = newColor.hsla.s;
|
||||
this.lightness = newColor.hsla.l;
|
||||
this.brightness = this.getBrightness(newColor.hsla.l);
|
||||
this.alpha = newColor.hsla.a * 100;
|
||||
this.hue = newColor.hsva.h;
|
||||
this.saturation = newColor.hsva.s;
|
||||
this.brightness = newColor.hsva.v;
|
||||
this.alpha = newColor.hsva.a * 100;
|
||||
this.syncValues();
|
||||
} else {
|
||||
this.inputValue = oldValue;
|
||||
this.inputValue = oldValue ?? '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.value !== this.lastValueEmitted) {
|
||||
this.emit('sl-change');
|
||||
this.lastValueEmitted = this.value;
|
||||
/** Returns the current value as a string in the specified format. */
|
||||
getFormattedValue(format: 'hex' | 'hexa' | 'rgb' | 'rgba' | 'hsl' | 'hsla' | 'hsv' | 'hsva' = 'hex') {
|
||||
const currentColor = this.parseColor(
|
||||
`hsva(${this.hue}, ${this.saturation}%, ${this.brightness}%, ${this.alpha / 100})`
|
||||
);
|
||||
|
||||
if (currentColor === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
switch (format) {
|
||||
case 'hex':
|
||||
return currentColor.hex;
|
||||
case 'hexa':
|
||||
return currentColor.hexa;
|
||||
case 'rgb':
|
||||
return currentColor.rgb.string;
|
||||
case 'rgba':
|
||||
return currentColor.rgba.string;
|
||||
case 'hsl':
|
||||
return currentColor.hsl.string;
|
||||
case 'hsla':
|
||||
return currentColor.hsla.string;
|
||||
case 'hsv':
|
||||
return currentColor.hsv.string;
|
||||
case 'hsva':
|
||||
return currentColor.hsva.string;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/** Checks for validity but does not show the browser's validation message. */
|
||||
checkValidity() {
|
||||
return this.input.checkValidity();
|
||||
}
|
||||
|
||||
/** Checks for validity and shows the browser's validation message if the control is invalid. */
|
||||
reportValidity() {
|
||||
if (!this.inline && !this.checkValidity()) {
|
||||
// If the input is inline and invalid, show the dropdown so the browser can focus on it
|
||||
this.dropdown.show();
|
||||
this.addEventListener('sl-after-show', () => this.input.reportValidity(), { once: true });
|
||||
return this.checkValidity();
|
||||
}
|
||||
|
||||
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 gridHandleX = this.saturation;
|
||||
const gridHandleY = 100 - this.brightness;
|
||||
const swatches = Array.isArray(this.swatches)
|
||||
? this.swatches // allow arrays for legacy purposes
|
||||
: this.swatches.split(';').filter(color => color.trim() !== '');
|
||||
|
||||
const colorPicker = html`
|
||||
<div
|
||||
@@ -700,9 +714,9 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
<div
|
||||
part="grid"
|
||||
class="color-picker__grid"
|
||||
style=${styleMap({ backgroundColor: `hsl(${this.hue}deg, 100%, 50%)` })}
|
||||
@mousedown=${this.handleGridDrag}
|
||||
@touchstart=${this.handleGridDrag}
|
||||
style=${styleMap({ backgroundColor: this.getHexString(this.hue, 100, 100) })}
|
||||
@pointerdown=${this.handleGridDrag}
|
||||
@touchmove=${this.handleTouchMove}
|
||||
>
|
||||
<span
|
||||
part="grid-handle"
|
||||
@@ -713,10 +727,10 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
style=${styleMap({
|
||||
top: `${gridHandleY}%`,
|
||||
left: `${gridHandleX}%`,
|
||||
backgroundColor: `hsla(${this.hue}deg, ${this.saturation}%, ${this.lightness}%)`
|
||||
backgroundColor: this.getHexString(this.hue, this.saturation, this.brightness, this.alpha)
|
||||
})}
|
||||
role="application"
|
||||
aria-label="HSL"
|
||||
aria-label="HSV"
|
||||
tabindex=${ifDefined(this.disabled ? undefined : '0')}
|
||||
@keydown=${this.handleGridKeyDown}
|
||||
></span>
|
||||
@@ -727,11 +741,11 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
<div
|
||||
part="slider hue-slider"
|
||||
class="color-picker__hue color-picker__slider"
|
||||
@mousedown=${this.handleHueDrag}
|
||||
@touchstart=${this.handleHueDrag}
|
||||
@pointerdown=${this.handleHueDrag}
|
||||
@touchmove=${this.handleTouchMove}
|
||||
>
|
||||
<span
|
||||
part="slider-handle"
|
||||
part="slider-handle hue-slider-handle"
|
||||
class="color-picker__slider-handle"
|
||||
style=${styleMap({
|
||||
left: `${this.hue === 0 ? 0 : 100 / (360 / this.hue)}%`
|
||||
@@ -752,21 +766,21 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
<div
|
||||
part="slider opacity-slider"
|
||||
class="color-picker__alpha color-picker__slider color-picker__transparent-bg"
|
||||
@mousedown="${this.handleAlphaDrag}"
|
||||
@touchstart="${this.handleAlphaDrag}"
|
||||
@pointerdown="${this.handleAlphaDrag}"
|
||||
@touchmove=${this.handleTouchMove}
|
||||
>
|
||||
<div
|
||||
class="color-picker__alpha-gradient"
|
||||
style=${styleMap({
|
||||
backgroundImage: `linear-gradient(
|
||||
to right,
|
||||
hsl(${this.hue}deg, ${this.saturation}%, ${this.lightness}%, 0%) 0%,
|
||||
hsl(${this.hue}deg, ${this.saturation}%, ${this.lightness}%) 100%
|
||||
${this.getHexString(this.hue, this.saturation, this.brightness, 0)} 0%
|
||||
${this.getHexString(this.hue, this.saturation, this.brightness, 100)} 100%
|
||||
)`
|
||||
})}
|
||||
></div>
|
||||
<span
|
||||
part="slider-handle"
|
||||
part="slider-handle opacity-slider-handle"
|
||||
class="color-picker__slider-handle"
|
||||
style=${styleMap({
|
||||
left: `${this.alpha}%`
|
||||
@@ -791,7 +805,7 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
class="color-picker__preview color-picker__transparent-bg"
|
||||
aria-label=${this.localize.term('copy')}
|
||||
style=${styleMap({
|
||||
'--preview-color': `hsla(${this.hue}deg, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})`
|
||||
'--preview-color': this.getHexString(this.hue, this.saturation, this.brightness, this.alpha)
|
||||
})}
|
||||
@click=${this.handleCopy}
|
||||
></button>
|
||||
@@ -806,11 +820,12 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false"
|
||||
.value=${live(this.isEmpty ? '' : this.inputValue)}
|
||||
value=${this.isEmpty ? '' : this.inputValue}
|
||||
?disabled=${this.disabled}
|
||||
aria-label=${this.localize.term('currentValue')}
|
||||
@keydown=${this.handleInputKeyDown}
|
||||
@sl-change=${this.handleInputChange}
|
||||
@sl-input=${this.handleInputInput}
|
||||
></sl-input>
|
||||
|
||||
<sl-button-group>
|
||||
@@ -856,10 +871,18 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
</sl-button-group>
|
||||
</div>
|
||||
|
||||
${this.swatches.length > 0
|
||||
${swatches.length > 0
|
||||
? html`
|
||||
<div part="swatches" class="color-picker__swatches">
|
||||
${this.swatches.map(swatch => {
|
||||
${swatches.map(swatch => {
|
||||
const parsedColor = this.parseColor(swatch);
|
||||
|
||||
// If we can't parse it, skip it
|
||||
if (!parsedColor) {
|
||||
console.error(`Unable to parse swatch color: "${swatch}"`, this);
|
||||
return '';
|
||||
}
|
||||
|
||||
return html`
|
||||
<div
|
||||
part="swatch"
|
||||
@@ -867,11 +890,14 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
tabindex=${ifDefined(this.disabled ? undefined : '0')}
|
||||
role="button"
|
||||
aria-label=${swatch}
|
||||
@click=${() => !this.disabled && this.setColor(swatch)}
|
||||
@click=${() => this.selectSwatch(swatch)}
|
||||
@keydown=${(event: KeyboardEvent) =>
|
||||
!this.disabled && event.key === 'Enter' && this.setColor(swatch)}
|
||||
!this.disabled && event.key === 'Enter' && this.setColor(parsedColor.hexa)}
|
||||
>
|
||||
<div class="color-picker__swatch-color" style=${styleMap({ backgroundColor: swatch })}></div>
|
||||
<div
|
||||
class="color-picker__swatch-color"
|
||||
style=${styleMap({ backgroundColor: parsedColor.hexa })}
|
||||
></div>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
@@ -909,7 +935,7 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
'color-picker__transparent-bg': true
|
||||
})}
|
||||
style=${styleMap({
|
||||
color: `hsla(${this.hue}deg, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})`
|
||||
color: this.getHexString(this.hue, this.saturation, this.brightness, this.alpha)
|
||||
})}
|
||||
type="button"
|
||||
>
|
||||
@@ -923,11 +949,6 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
|
||||
}
|
||||
}
|
||||
|
||||
function toHex(value: number) {
|
||||
const hex = Math.round(value).toString(16);
|
||||
return hex.length === 1 ? `0${hex}` : hex;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-color-picker': SlColorPicker;
|
||||
|
||||
@@ -56,11 +56,20 @@ export default css`
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: var(--sl-transition-medium) transform ease;
|
||||
transition: var(--sl-transition-medium) rotate ease;
|
||||
}
|
||||
|
||||
.details--open .details__summary-icon {
|
||||
transform: rotate(90deg);
|
||||
rotate: 90deg;
|
||||
}
|
||||
|
||||
.details--open.details--rtl .details__summary-icon {
|
||||
rotate: -90deg;
|
||||
}
|
||||
|
||||
.details--open slot[name='expand-icon'],
|
||||
.details:not(.details--open) slot[name='collapse-icon'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.details__body {
|
||||
@@ -68,6 +77,7 @@ export default css`
|
||||
}
|
||||
|
||||
.details__content {
|
||||
display: block;
|
||||
padding: var(--sl-spacing-medium);
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -183,7 +183,7 @@ describe('<sl-details>', () => {
|
||||
await first.show();
|
||||
await second.show();
|
||||
|
||||
expect(firstBody.clientHeight).to.equal(200);
|
||||
expect(secondBody.clientHeight).to.equal(400);
|
||||
expect(firstBody.clientHeight).to.equal(232); // 200 + 16px + 16px (vertical padding)
|
||||
expect(secondBody.clientHeight).to.equal(432); // 400 + 16px + 16px (vertical padding)
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,36 +1,38 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { animateTo, shimKeyframesHeightAuto, stopAnimations } from '../../internal/animate';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import '../icon/icon';
|
||||
import { animateTo, shimKeyframesHeightAuto, stopAnimations } from '../../internal/animate';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
|
||||
import { html } from 'lit';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import styles from './details.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
* @summary Details show a brief summary and expand to show additional content.
|
||||
*
|
||||
* @since 2.0
|
||||
* @documentation https://shoelace.style/components/details
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @dependency sl-icon
|
||||
*
|
||||
* @slot - The details' content.
|
||||
* @slot - The details' main content.
|
||||
* @slot summary - The details' summary. Alternatively, you can use the `summary` attribute.
|
||||
* @slot expand-icon - Optional expand icon to use instead of the default. Works best with `<sl-icon>`.
|
||||
* @slot collapse-icon - Optional collapse icon to use instead of the default. Works best with `<sl-icon>`.
|
||||
*
|
||||
* @event sl-show - Emitted when the details opens.
|
||||
* @event sl-after-show - Emitted after the details opens and all animations are complete.
|
||||
* @event sl-hide - Emitted when the details closes.
|
||||
* @event sl-after-hide - Emitted after the details closes and all animations are complete.
|
||||
*
|
||||
* @csspart base - The component's internal wrapper.
|
||||
* @csspart header - The summary header.
|
||||
* @csspart summary - The details summary.
|
||||
* @csspart summary-icon - The expand/collapse summary icon.
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart header - The header that wraps both the summary and the expand/collapse icon.
|
||||
* @csspart summary - The container that wraps the summary.
|
||||
* @csspart summary-icon - The container that wraps the expand/collapse icons.
|
||||
* @csspart content - The details content.
|
||||
*
|
||||
* @animation details.show - The animation to use when showing details. You can use `height: auto` with this animation.
|
||||
@@ -40,16 +42,20 @@ import type { CSSResultGroup } from 'lit';
|
||||
export default class SlDetails extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
@query('.details') details: HTMLElement;
|
||||
@query('.details__header') header: HTMLElement;
|
||||
@query('.details__body') body: HTMLElement;
|
||||
@query('.details__expand-icon-slot') expandIconSlot: HTMLSlotElement;
|
||||
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
/** Indicates whether or not the details is open. You can use this in lieu of the show/hide methods. */
|
||||
/**
|
||||
* Indicates whether or not the details is open. You can toggle this attribute to show and hide the details, or you
|
||||
* can use the `show()` and `hide()` methods and this attribute will reflect the details' open state.
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true }) open = false;
|
||||
|
||||
/** The summary to show in the details header. If you need to display HTML, use the `summary` slot instead. */
|
||||
/** The summary to show in the header. If you need to display HTML, use the `summary` slot instead. */
|
||||
@property() summary: string;
|
||||
|
||||
/** Disables the details so it can't be toggled. */
|
||||
@@ -60,27 +66,7 @@ export default class SlDetails extends ShoelaceElement {
|
||||
this.body.style.height = this.open ? 'auto' : '0';
|
||||
}
|
||||
|
||||
/** Shows the details. */
|
||||
async show() {
|
||||
if (this.open || this.disabled) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = true;
|
||||
return waitForEvent(this, 'sl-after-show');
|
||||
}
|
||||
|
||||
/** Hides the details */
|
||||
async hide() {
|
||||
if (!this.open || this.disabled) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = false;
|
||||
return waitForEvent(this, 'sl-after-hide');
|
||||
}
|
||||
|
||||
handleSummaryClick() {
|
||||
private handleSummaryClick() {
|
||||
if (!this.disabled) {
|
||||
if (this.open) {
|
||||
this.hide();
|
||||
@@ -92,7 +78,7 @@ export default class SlDetails extends ShoelaceElement {
|
||||
}
|
||||
}
|
||||
|
||||
handleSummaryKeyDown(event: KeyboardEvent) {
|
||||
private handleSummaryKeyDown(event: KeyboardEvent) {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -151,14 +137,37 @@ export default class SlDetails extends ShoelaceElement {
|
||||
}
|
||||
}
|
||||
|
||||
/** Shows the details. */
|
||||
async show() {
|
||||
if (this.open || this.disabled) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = true;
|
||||
return waitForEvent(this, 'sl-after-show');
|
||||
}
|
||||
|
||||
/** Hides the details */
|
||||
async hide() {
|
||||
if (!this.open || this.disabled) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = false;
|
||||
return waitForEvent(this, 'sl-after-hide');
|
||||
}
|
||||
|
||||
render() {
|
||||
const isRtl = this.localize.dir() === 'rtl';
|
||||
|
||||
return html`
|
||||
<div
|
||||
part="base"
|
||||
class=${classMap({
|
||||
details: true,
|
||||
'details--open': this.open,
|
||||
'details--disabled': this.disabled
|
||||
'details--disabled': this.disabled,
|
||||
'details--rtl': isRtl
|
||||
})}
|
||||
>
|
||||
<header
|
||||
@@ -173,19 +182,20 @@ export default class SlDetails extends ShoelaceElement {
|
||||
@click=${this.handleSummaryClick}
|
||||
@keydown=${this.handleSummaryKeyDown}
|
||||
>
|
||||
<div part="summary" class="details__summary">
|
||||
<slot name="summary">${this.summary}</slot>
|
||||
</div>
|
||||
<slot name="summary" part="summary" class="details__summary">${this.summary}</slot>
|
||||
|
||||
<span part="summary-icon" class="details__summary-icon">
|
||||
<sl-icon name="chevron-right" library="system"></sl-icon>
|
||||
<slot name="expand-icon">
|
||||
<sl-icon library="system" name=${isRtl ? 'chevron-left' : 'chevron-right'}></sl-icon>
|
||||
</slot>
|
||||
<slot name="collapse-icon">
|
||||
<sl-icon library="system" name=${isRtl ? 'chevron-left' : 'chevron-right'}></sl-icon>
|
||||
</slot>
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div class="details__body">
|
||||
<div part="content" id="content" class="details__content" role="region" aria-labelledby="header">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<slot part="content" id="content" class="details__content" role="region" aria-labelledby="header"></slot>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -51,7 +51,6 @@ export default css`
|
||||
.dialog--open .dialog__panel {
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.dialog__header {
|
||||
@@ -68,16 +67,26 @@ export default css`
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dialog__close {
|
||||
.dialog__header-actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: end;
|
||||
gap: var(--sl-spacing-2x-small);
|
||||
padding: 0 var(--header-spacing);
|
||||
}
|
||||
|
||||
.dialog__header-actions sl-icon-button,
|
||||
.dialog__header-actions ::slotted(sl-icon-button) {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--sl-font-size-x-large);
|
||||
padding: 0 var(--header-spacing);
|
||||
font-size: var(--sl-font-size-medium);
|
||||
}
|
||||
|
||||
.dialog__body {
|
||||
flex: 1 1 auto;
|
||||
display: block;
|
||||
padding: var(--body-spacing);
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import Modal from '../../internal/modal';
|
||||
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import '../icon-button/icon-button';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { html } from 'lit';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import Modal from '../../internal/modal';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import styles from './dialog.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
* @summary Dialogs, sometimes called "modals", appear above the page and require the user's immediate attention.
|
||||
*
|
||||
* @since 2.0
|
||||
* @documentation https://shoelace.style/components/dialog
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @dependency sl-icon-button
|
||||
*
|
||||
* @slot - The dialog's content.
|
||||
* @slot - The dialog's main content.
|
||||
* @slot label - The dialog's label. Alternatively, you can use the `label` attribute.
|
||||
* @slot header-actions - Optional actions to add to the header. Works best with `<sl-icon-button>`.
|
||||
* @slot footer - The dialog's footer, usually one or more buttons representing various options.
|
||||
*
|
||||
* @event sl-show - Emitted when the dialog opens.
|
||||
@@ -38,15 +39,16 @@ import type { CSSResultGroup } from 'lit';
|
||||
* `event.preventDefault()` will keep the dialog open. Avoid using this unless closing the dialog will result in
|
||||
* destructive behavior such as data loss.
|
||||
*
|
||||
* @csspart base - The component's internal wrapper.
|
||||
* @csspart overlay - The overlay.
|
||||
* @csspart panel - The dialog panel (where the dialog and its content is rendered).
|
||||
* @csspart header - The dialog header.
|
||||
* @csspart title - The dialog title.
|
||||
* @csspart close-button - The close button.
|
||||
* @csspart close-button__base - The close button's `base` part.
|
||||
* @csspart body - The dialog body.
|
||||
* @csspart footer - The dialog footer.
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart overlay - The overlay that covers the screen behind the dialog.
|
||||
* @csspart panel - The dialog's panel (where the dialog and its content are rendered).
|
||||
* @csspart header - The dialog's header. This element wraps the title and header actions.
|
||||
* @csspart header-actions - Optional actions to add to the header. Works best with `<sl-icon-button>`.
|
||||
* @csspart title - The dialog's title.
|
||||
* @csspart close-button - The close button, an `<sl-icon-button>`.
|
||||
* @csspart close-button__base - The close button's exported `base` part.
|
||||
* @csspart body - The dialog's body.
|
||||
* @csspart footer - The dialog's footer.
|
||||
*
|
||||
* @cssproperty --width - The preferred width of the dialog. Note that the dialog will shrink to accommodate smaller screens.
|
||||
* @cssproperty --header-spacing - The amount of padding to use for the header.
|
||||
@@ -63,22 +65,24 @@ import type { CSSResultGroup } from 'lit';
|
||||
export default class SlDialog extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
@query('.dialog') dialog: HTMLElement;
|
||||
@query('.dialog__panel') panel: HTMLElement;
|
||||
@query('.dialog__overlay') overlay: HTMLElement;
|
||||
|
||||
private readonly hasSlotController = new HasSlotController(this, 'footer');
|
||||
private readonly localize = new LocalizeController(this);
|
||||
private modal: Modal;
|
||||
private originalTrigger: HTMLElement | null;
|
||||
|
||||
/** Indicates whether or not the dialog is open. You can use this in lieu of the show/hide methods. */
|
||||
@query('.dialog') dialog: HTMLElement;
|
||||
@query('.dialog__panel') panel: HTMLElement;
|
||||
@query('.dialog__overlay') overlay: HTMLElement;
|
||||
|
||||
/**
|
||||
* Indicates whether or not the dialog is open. You can toggle this attribute to show and hide the dialog, or you can
|
||||
* use the `show()` and `hide()` methods and this attribute will reflect the dialog's open state.
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true }) open = false;
|
||||
|
||||
/**
|
||||
* The dialog's label as displayed in the header. You should always include a relevant label even when using
|
||||
* `no-header`, as it is required for proper accessibility. If you need to display HTML, you can use the `label` slot
|
||||
* instead.
|
||||
* `no-header`, as it is required for proper accessibility. If you need to display HTML, use the `label` slot instead.
|
||||
*/
|
||||
@property({ reflect: true }) label = '';
|
||||
|
||||
@@ -109,26 +113,6 @@ export default class SlDialog extends ShoelaceElement {
|
||||
unlockBodyScrolling(this);
|
||||
}
|
||||
|
||||
/** Shows the dialog. */
|
||||
async show() {
|
||||
if (this.open) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = true;
|
||||
return waitForEvent(this, 'sl-after-show');
|
||||
}
|
||||
|
||||
/** Hides the dialog */
|
||||
async hide() {
|
||||
if (!this.open) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = false;
|
||||
return waitForEvent(this, 'sl-after-hide');
|
||||
}
|
||||
|
||||
private requestClose(source: 'close-button' | 'keyboard' | 'overlay') {
|
||||
const slRequestClose = this.emit('sl-request-close', {
|
||||
cancelable: true,
|
||||
@@ -144,15 +128,15 @@ export default class SlDialog extends ShoelaceElement {
|
||||
this.hide();
|
||||
}
|
||||
|
||||
addOpenListeners() {
|
||||
private addOpenListeners() {
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
}
|
||||
|
||||
removeOpenListeners() {
|
||||
private removeOpenListeners() {
|
||||
document.removeEventListener('keydown', this.handleDocumentKeyDown);
|
||||
}
|
||||
|
||||
handleDocumentKeyDown(event: KeyboardEvent) {
|
||||
private handleDocumentKeyDown(event: KeyboardEvent) {
|
||||
if (this.open && event.key === 'Escape') {
|
||||
event.stopPropagation();
|
||||
this.requestClose('keyboard');
|
||||
@@ -251,6 +235,26 @@ export default class SlDialog extends ShoelaceElement {
|
||||
}
|
||||
}
|
||||
|
||||
/** Shows the dialog. */
|
||||
async show() {
|
||||
if (this.open) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = true;
|
||||
return waitForEvent(this, 'sl-after-show');
|
||||
}
|
||||
|
||||
/** Hides the dialog */
|
||||
async hide() {
|
||||
if (!this.open) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = false;
|
||||
return waitForEvent(this, 'sl-after-hide');
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div
|
||||
@@ -279,22 +283,23 @@ export default class SlDialog extends ShoelaceElement {
|
||||
<h2 part="title" class="dialog__title" id="title">
|
||||
<slot name="label"> ${this.label.length > 0 ? this.label : String.fromCharCode(65279)} </slot>
|
||||
</h2>
|
||||
<sl-icon-button
|
||||
part="close-button"
|
||||
exportparts="base:close-button__base"
|
||||
class="dialog__close"
|
||||
name="x"
|
||||
label=${this.localize.term('close')}
|
||||
library="system"
|
||||
@click="${() => this.requestClose('close-button')}"
|
||||
></sl-icon-button>
|
||||
<div part="header-actions" class="dialog__header-actions">
|
||||
<slot name="header-actions"></slot>
|
||||
<sl-icon-button
|
||||
part="close-button"
|
||||
exportparts="base:close-button__base"
|
||||
class="dialog__close"
|
||||
name="x-lg"
|
||||
label=${this.localize.term('close')}
|
||||
library="system"
|
||||
@click="${() => this.requestClose('close-button')}"
|
||||
></sl-icon-button>
|
||||
</div>
|
||||
</header>
|
||||
`
|
||||
: ''}
|
||||
|
||||
<div part="body" class="dialog__body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<slot part="body" class="dialog__body"></slot>
|
||||
|
||||
<footer part="footer" class="dialog__footer">
|
||||
<slot name="footer"></slot>
|
||||
@@ -307,22 +312,22 @@ export default class SlDialog extends ShoelaceElement {
|
||||
|
||||
setDefaultAnimation('dialog.show', {
|
||||
keyframes: [
|
||||
{ opacity: 0, transform: 'scale(0.8)' },
|
||||
{ opacity: 1, transform: 'scale(1)' }
|
||||
{ opacity: 0, scale: 0.8 },
|
||||
{ opacity: 1, scale: 1 }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
setDefaultAnimation('dialog.hide', {
|
||||
keyframes: [
|
||||
{ opacity: 1, transform: 'scale(1)' },
|
||||
{ opacity: 0, transform: 'scale(0.8)' }
|
||||
{ opacity: 1, scale: 1 },
|
||||
{ opacity: 0, scale: 0.8 }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
setDefaultAnimation('dialog.denyClose', {
|
||||
keyframes: [{ transform: 'scale(1)' }, { transform: 'scale(1.02)' }, { transform: 'scale(1)' }],
|
||||
keyframes: [{ scale: 1 }, { scale: 1.02 }, { scale: 1 }],
|
||||
options: { duration: 250 }
|
||||
});
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { watch } from '../../internal/watch';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import styles from './divider.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
* @summary Dividers are used to visually separate or group elements.
|
||||
*
|
||||
* @since 2.0
|
||||
* @documentation https://shoelace.style/components/divider
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @cssproperty --color - The color of the divider.
|
||||
* @cssproperty --width - The width of the divider.
|
||||
|
||||
@@ -41,7 +41,6 @@ export default css`
|
||||
max-height: 100%;
|
||||
background-color: var(--sl-panel-background-color);
|
||||
box-shadow: var(--sl-shadow-x-large);
|
||||
transition: var(--sl-transition-medium) transform;
|
||||
overflow: auto;
|
||||
pointer-events: all;
|
||||
}
|
||||
@@ -99,16 +98,26 @@ export default css`
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.drawer__close {
|
||||
.drawer__header-actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: end;
|
||||
gap: var(--sl-spacing-2x-small);
|
||||
padding: 0 var(--header-spacing);
|
||||
}
|
||||
|
||||
.drawer__header-actions sl-icon-button,
|
||||
.drawer__header-actions ::slotted(sl-icon-button) {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: var(--sl-font-size-x-large);
|
||||
padding: 0 var(--header-spacing);
|
||||
font-size: var(--sl-font-size-medium);
|
||||
}
|
||||
|
||||
.drawer__body {
|
||||
flex: 1 1 auto;
|
||||
display: block;
|
||||
padding: var(--body-spacing);
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
@@ -139,7 +148,7 @@ export default css`
|
||||
}
|
||||
|
||||
.drawer--contained .drawer__overlay {
|
||||
position: absolute;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
|
||||
@@ -1,31 +1,32 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import Modal from '../../internal/modal';
|
||||
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { uppercaseFirstLetter } from '../../internal/string';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import '../icon-button/icon-button';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { html } from 'lit';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll';
|
||||
import { uppercaseFirstLetter } from '../../internal/string';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import Modal from '../../internal/modal';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import styles from './drawer.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
* @summary Drawers slide in from a container to expose additional options and information.
|
||||
*
|
||||
* @since 2.0
|
||||
* @documentation https://shoelace.style/components/drawer
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @dependency sl-icon-button
|
||||
*
|
||||
* @slot - The drawer's content.
|
||||
* @slot - The drawer's main content.
|
||||
* @slot label - The drawer's label. Alternatively, you can use the `label` attribute.
|
||||
* @slot header-actions - Optional actions to add to the header. Works best with `<sl-icon-button>`.
|
||||
* @slot footer - The drawer's footer, usually one or more buttons representing various options.
|
||||
*
|
||||
* @event sl-show - Emitted when the drawer opens.
|
||||
@@ -39,15 +40,16 @@ import type { CSSResultGroup } from 'lit';
|
||||
* `event.preventDefault()` will keep the drawer open. Avoid using this unless closing the drawer will result in
|
||||
* destructive behavior such as data loss.
|
||||
*
|
||||
* @csspart base - The component's internal wrapper.
|
||||
* @csspart overlay - The overlay.
|
||||
* @csspart panel - The drawer panel (where the drawer and its content is rendered).
|
||||
* @csspart header - The drawer header.
|
||||
* @csspart title - The drawer title.
|
||||
* @csspart close-button - The close button.
|
||||
* @csspart close-button__base - The close button's `base` part.
|
||||
* @csspart body - The drawer body.
|
||||
* @csspart footer - The drawer footer.
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart overlay - The overlay that covers the screen behind the drawer.
|
||||
* @csspart panel - The drawer's panel (where the drawer and its content are rendered).
|
||||
* @csspart header - The drawer's header. This element wraps the title and header actions.
|
||||
* @csspart header-actions - Optional actions to add to the header. Works best with `<sl-icon-button>`.
|
||||
* @csspart title - The drawer's title.
|
||||
* @csspart close-button - The close button, an `<sl-icon-button>`.
|
||||
* @csspart close-button__base - The close button's exported `base` part.
|
||||
* @csspart body - The drawer's body.
|
||||
* @csspart footer - The drawer's footer.
|
||||
*
|
||||
* @cssproperty --size - The preferred size of the drawer. This will be applied to the drawer's width or height
|
||||
* depending on its `placement`. Note that the drawer will shrink to accommodate smaller screens.
|
||||
@@ -71,22 +73,24 @@ import type { CSSResultGroup } from 'lit';
|
||||
export default class SlDrawer extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
@query('.drawer') drawer: HTMLElement;
|
||||
@query('.drawer__panel') panel: HTMLElement;
|
||||
@query('.drawer__overlay') overlay: HTMLElement;
|
||||
|
||||
private readonly hasSlotController = new HasSlotController(this, 'footer');
|
||||
private readonly localize = new LocalizeController(this);
|
||||
private modal: Modal;
|
||||
private originalTrigger: HTMLElement | null;
|
||||
|
||||
/** Indicates whether or not the drawer is open. You can use this in lieu of the show/hide methods. */
|
||||
@query('.drawer') drawer: HTMLElement;
|
||||
@query('.drawer__panel') panel: HTMLElement;
|
||||
@query('.drawer__overlay') overlay: HTMLElement;
|
||||
|
||||
/**
|
||||
* Indicates whether or not the drawer is open. You can toggle this attribute to show and hide the drawer, or you can
|
||||
* use the `show()` and `hide()` methods and this attribute will reflect the drawer's open state.
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true }) open = false;
|
||||
|
||||
/**
|
||||
* The drawer's label as displayed in the header. You should always include a relevant label even when using
|
||||
* `no-header`, as it is required for proper accessibility. If you need to display HTML, you can use the `label` slot
|
||||
* instead.
|
||||
* `no-header`, as it is required for proper accessibility. If you need to display HTML, use the `label` slot instead.
|
||||
*/
|
||||
@property({ reflect: true }) label = '';
|
||||
|
||||
@@ -114,10 +118,13 @@ export default class SlDrawer extends ShoelaceElement {
|
||||
firstUpdated() {
|
||||
this.drawer.hidden = !this.open;
|
||||
|
||||
if (this.open && !this.contained) {
|
||||
if (this.open) {
|
||||
this.addOpenListeners();
|
||||
this.modal.activate();
|
||||
lockBodyScrolling(this);
|
||||
|
||||
if (!this.contained) {
|
||||
this.modal.activate();
|
||||
lockBodyScrolling(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,26 +133,6 @@ export default class SlDrawer extends ShoelaceElement {
|
||||
unlockBodyScrolling(this);
|
||||
}
|
||||
|
||||
/** Shows the drawer. */
|
||||
async show() {
|
||||
if (this.open) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = true;
|
||||
return waitForEvent(this, 'sl-after-show');
|
||||
}
|
||||
|
||||
/** Hides the drawer */
|
||||
async hide() {
|
||||
if (!this.open) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = false;
|
||||
return waitForEvent(this, 'sl-after-hide');
|
||||
}
|
||||
|
||||
private requestClose(source: 'close-button' | 'keyboard' | 'overlay') {
|
||||
const slRequestClose = this.emit('sl-request-close', {
|
||||
cancelable: true,
|
||||
@@ -161,16 +148,16 @@ export default class SlDrawer extends ShoelaceElement {
|
||||
this.hide();
|
||||
}
|
||||
|
||||
addOpenListeners() {
|
||||
private addOpenListeners() {
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
}
|
||||
|
||||
removeOpenListeners() {
|
||||
private removeOpenListeners() {
|
||||
document.removeEventListener('keydown', this.handleDocumentKeyDown);
|
||||
}
|
||||
|
||||
handleDocumentKeyDown(event: KeyboardEvent) {
|
||||
if (this.open && event.key === 'Escape') {
|
||||
private handleDocumentKeyDown(event: KeyboardEvent) {
|
||||
if (this.open && !this.contained && event.key === 'Escape') {
|
||||
event.stopPropagation();
|
||||
this.requestClose('keyboard');
|
||||
}
|
||||
@@ -237,8 +224,11 @@ export default class SlDrawer extends ShoelaceElement {
|
||||
// Hide
|
||||
this.emit('sl-hide');
|
||||
this.removeOpenListeners();
|
||||
this.modal.deactivate();
|
||||
unlockBodyScrolling(this);
|
||||
|
||||
if (!this.contained) {
|
||||
this.modal.deactivate();
|
||||
unlockBodyScrolling(this);
|
||||
}
|
||||
|
||||
await Promise.all([stopAnimations(this.drawer), stopAnimations(this.overlay)]);
|
||||
const panelAnimation = getAnimation(this, `drawer.hide${uppercaseFirstLetter(this.placement)}`, {
|
||||
@@ -274,6 +264,39 @@ export default class SlDrawer extends ShoelaceElement {
|
||||
}
|
||||
}
|
||||
|
||||
@watch('contained', { waitUntilFirstUpdate: true })
|
||||
handleNoModalChange() {
|
||||
if (this.open && !this.contained) {
|
||||
this.modal.activate();
|
||||
lockBodyScrolling(this);
|
||||
}
|
||||
|
||||
if (this.open && this.contained) {
|
||||
this.modal.deactivate();
|
||||
unlockBodyScrolling(this);
|
||||
}
|
||||
}
|
||||
|
||||
/** Shows the drawer. */
|
||||
async show() {
|
||||
if (this.open) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = true;
|
||||
return waitForEvent(this, 'sl-after-show');
|
||||
}
|
||||
|
||||
/** Hides the drawer */
|
||||
async hide() {
|
||||
if (!this.open) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.open = false;
|
||||
return waitForEvent(this, 'sl-after-hide');
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div
|
||||
@@ -310,22 +333,23 @@ export default class SlDrawer extends ShoelaceElement {
|
||||
<!-- If there's no label, use an invisible character to prevent the header from collapsing -->
|
||||
<slot name="label"> ${this.label.length > 0 ? this.label : String.fromCharCode(65279)} </slot>
|
||||
</h2>
|
||||
<sl-icon-button
|
||||
part="close-button"
|
||||
exportparts="base:close-button__base"
|
||||
class="drawer__close"
|
||||
name="x"
|
||||
label=${this.localize.term('close')}
|
||||
library="system"
|
||||
@click=${() => this.requestClose('close-button')}
|
||||
></sl-icon-button>
|
||||
<div part="header-actions" class="drawer__header-actions">
|
||||
<slot name="header-actions"></slot>
|
||||
<sl-icon-button
|
||||
part="close-button"
|
||||
exportparts="base:close-button__base"
|
||||
class="drawer__close"
|
||||
name="x-lg"
|
||||
label=${this.localize.term('close')}
|
||||
library="system"
|
||||
@click=${() => this.requestClose('close-button')}
|
||||
></sl-icon-button>
|
||||
</div>
|
||||
</header>
|
||||
`
|
||||
: ''}
|
||||
|
||||
<div part="body" class="drawer__body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<slot part="body" class="drawer__body"></slot>
|
||||
|
||||
<footer part="footer" class="drawer__footer">
|
||||
<slot name="footer"></slot>
|
||||
@@ -339,16 +363,16 @@ export default class SlDrawer extends ShoelaceElement {
|
||||
// Top
|
||||
setDefaultAnimation('drawer.showTop', {
|
||||
keyframes: [
|
||||
{ opacity: 0, transform: 'translateY(-100%)' },
|
||||
{ opacity: 1, transform: 'translateY(0)' }
|
||||
{ opacity: 0, translate: '0 -100%' },
|
||||
{ opacity: 1, translate: '0 0' }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
setDefaultAnimation('drawer.hideTop', {
|
||||
keyframes: [
|
||||
{ opacity: 1, transform: 'translateY(0)' },
|
||||
{ opacity: 0, transform: 'translateY(-100%)' }
|
||||
{ opacity: 1, translate: '0 0' },
|
||||
{ opacity: 0, translate: '0 -100%' }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
@@ -356,24 +380,24 @@ setDefaultAnimation('drawer.hideTop', {
|
||||
// End
|
||||
setDefaultAnimation('drawer.showEnd', {
|
||||
keyframes: [
|
||||
{ opacity: 0, transform: 'translateX(100%)' },
|
||||
{ opacity: 1, transform: 'translateX(0)' }
|
||||
{ opacity: 0, translate: '100%' },
|
||||
{ opacity: 1, translate: '0' }
|
||||
],
|
||||
rtlKeyframes: [
|
||||
{ opacity: 0, transform: 'translateX(-100%)' },
|
||||
{ opacity: 1, transform: 'translateX(0)' }
|
||||
{ opacity: 0, translate: '-100%' },
|
||||
{ opacity: 1, translate: '0' }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
setDefaultAnimation('drawer.hideEnd', {
|
||||
keyframes: [
|
||||
{ opacity: 1, transform: 'translateX(0)' },
|
||||
{ opacity: 0, transform: 'translateX(100%)' }
|
||||
{ opacity: 1, translate: '0' },
|
||||
{ opacity: 0, translate: '100%' }
|
||||
],
|
||||
rtlKeyframes: [
|
||||
{ opacity: 1, transform: 'translateX(0)' },
|
||||
{ opacity: 0, transform: 'translateX(-100%)' }
|
||||
{ opacity: 1, translate: '0' },
|
||||
{ opacity: 0, translate: '-100%' }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
@@ -381,16 +405,16 @@ setDefaultAnimation('drawer.hideEnd', {
|
||||
// Bottom
|
||||
setDefaultAnimation('drawer.showBottom', {
|
||||
keyframes: [
|
||||
{ opacity: 0, transform: 'translateY(100%)' },
|
||||
{ opacity: 1, transform: 'translateY(0)' }
|
||||
{ opacity: 0, translate: '0 100%' },
|
||||
{ opacity: 1, translate: '0 0' }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
setDefaultAnimation('drawer.hideBottom', {
|
||||
keyframes: [
|
||||
{ opacity: 1, transform: 'translateY(0)' },
|
||||
{ opacity: 0, transform: 'translateY(100%)' }
|
||||
{ opacity: 1, translate: '0 0' },
|
||||
{ opacity: 0, translate: '0 100%' }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
@@ -398,31 +422,31 @@ setDefaultAnimation('drawer.hideBottom', {
|
||||
// Start
|
||||
setDefaultAnimation('drawer.showStart', {
|
||||
keyframes: [
|
||||
{ opacity: 0, transform: 'translateX(-100%)' },
|
||||
{ opacity: 1, transform: 'translateX(0)' }
|
||||
{ opacity: 0, translate: '-100%' },
|
||||
{ opacity: 1, translate: '0' }
|
||||
],
|
||||
rtlKeyframes: [
|
||||
{ opacity: 0, transform: 'translateX(100%)' },
|
||||
{ opacity: 1, transform: 'translateX(0)' }
|
||||
{ opacity: 0, translate: '100%' },
|
||||
{ opacity: 1, translate: '0' }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
setDefaultAnimation('drawer.hideStart', {
|
||||
keyframes: [
|
||||
{ opacity: 1, transform: 'translateX(0)' },
|
||||
{ opacity: 0, transform: 'translateX(-100%)' }
|
||||
{ opacity: 1, translate: '0' },
|
||||
{ opacity: 0, translate: '-100%' }
|
||||
],
|
||||
rtlKeyframes: [
|
||||
{ opacity: 1, transform: 'translateX(0)' },
|
||||
{ opacity: 0, transform: 'translateX(100%)' }
|
||||
{ opacity: 1, translate: '0' },
|
||||
{ opacity: 0, translate: '100%' }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
// Deny close
|
||||
setDefaultAnimation('drawer.denyClose', {
|
||||
keyframes: [{ transform: 'scale(1)' }, { transform: 'scale(1.01)' }, { transform: 'scale(1)' }],
|
||||
keyframes: [{ scale: 1 }, { scale: 1.01 }, { scale: 1 }],
|
||||
options: { duration: 250 }
|
||||
});
|
||||
|
||||
|
||||
@@ -36,12 +36,13 @@ export default css`
|
||||
font-family: var(--sl-font-sans);
|
||||
font-size: var(--sl-font-size-medium);
|
||||
font-weight: var(--sl-font-weight-normal);
|
||||
color: var(--color);
|
||||
box-shadow: var(--sl-shadow-large);
|
||||
border-radius: var(--sl-border-radius-medium);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dropdown--open .dropdown__panel {
|
||||
display: block;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import { scrollIntoView } from '../../internal/scroll';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { getTabbableBoundary } from '../../internal/tabbable';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import '../popup/popup';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
|
||||
import { getTabbableBoundary } from '../../internal/tabbable';
|
||||
import { html } from 'lit';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import { scrollIntoView } from '../../internal/scroll';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import styles from './dropdown.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
import type SlButton from '../button/button';
|
||||
import type SlIconButton from '../icon-button/icon-button';
|
||||
import type SlMenuItem from '../menu-item/menu-item';
|
||||
import type SlMenu from '../menu/menu';
|
||||
import type SlMenuItem from '../menu-item/menu-item';
|
||||
import type SlPopup from '../popup/popup';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
* @summary Dropdowns expose additional content that "drops down" in a panel.
|
||||
*
|
||||
* @since 2.0
|
||||
* @documentation https://shoelace.style/components/dropdown
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @dependency sl-popup
|
||||
*
|
||||
* @slot - The dropdown's content.
|
||||
* @slot - The dropdown's main content.
|
||||
* @slot trigger - The dropdown's trigger, usually a `<sl-button>` element.
|
||||
*
|
||||
* @event sl-show - Emitted when the dropdown opens.
|
||||
@@ -34,7 +34,7 @@ import type { CSSResultGroup } from 'lit';
|
||||
* @event sl-hide - Emitted when the dropdown closes.
|
||||
* @event sl-after-hide - Emitted after the dropdown closes and all animations are complete.
|
||||
*
|
||||
* @csspart base - The component's internal wrapper.
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart trigger - The container that wraps the trigger.
|
||||
* @csspart panel - The panel that gets shown when the dropdown is open.
|
||||
*
|
||||
@@ -46,12 +46,15 @@ export default class SlDropdown extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
@query('.dropdown') popup: SlPopup;
|
||||
@query('.dropdown__trigger') trigger: HTMLElement;
|
||||
@query('.dropdown__panel') panel: HTMLElement;
|
||||
@query('.dropdown__trigger') trigger: HTMLSlotElement;
|
||||
@query('.dropdown__panel') panel: HTMLSlotElement;
|
||||
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
/** Indicates whether or not the dropdown is open. You can use this in lieu of the show/hide methods. */
|
||||
/**
|
||||
* Indicates whether or not the dropdown is open. You can toggle this attribute to show and hide the dropdown, or you
|
||||
* can use the `show()` and `hide()` methods and this attribute will reflect the dropdown's open state.
|
||||
*/
|
||||
@property({ type: Boolean, reflect: true }) open = false;
|
||||
|
||||
/**
|
||||
@@ -77,11 +80,14 @@ export default class SlDropdown extends ShoelaceElement {
|
||||
|
||||
/**
|
||||
* By default, the dropdown is closed when an item is selected. This attribute will keep it open instead. Useful for
|
||||
* controls that allow multiple selections.
|
||||
* dropdowns that allow for multiple interactions.
|
||||
*/
|
||||
@property({ attribute: 'stay-open-on-select', type: Boolean, reflect: true }) stayOpenOnSelect = false;
|
||||
|
||||
/** The dropdown will close when the user interacts outside of this element (e.g. clicking). */
|
||||
/**
|
||||
* The dropdown will close when the user interacts outside of this element (e.g. clicking). Useful for composing other
|
||||
* components that use a dropdown internally.
|
||||
*/
|
||||
@property({ attribute: false }) containingElement?: HTMLElement;
|
||||
|
||||
/** The distance in pixels from which to offset the panel away from its trigger. */
|
||||
@@ -92,7 +98,7 @@ export default class SlDropdown extends ShoelaceElement {
|
||||
|
||||
/**
|
||||
* Enable this option to prevent the panel from being clipped when the component is placed inside a container with
|
||||
* `overflow: auto|scroll`.
|
||||
* `overflow: auto|scroll`. Hoisting uses a fixed positioning strategy that works in many, but not all, scenarios.
|
||||
*/
|
||||
@property({ type: Boolean }) hoist = false;
|
||||
|
||||
@@ -126,16 +132,14 @@ export default class SlDropdown extends ShoelaceElement {
|
||||
}
|
||||
|
||||
focusOnTrigger() {
|
||||
const slot = this.trigger.querySelector('slot')!;
|
||||
const trigger = slot.assignedElements({ flatten: true })[0] as HTMLElement | undefined;
|
||||
const trigger = this.trigger.assignedElements({ flatten: true })[0] as HTMLElement | undefined;
|
||||
if (typeof trigger?.focus === 'function') {
|
||||
trigger.focus();
|
||||
}
|
||||
}
|
||||
|
||||
getMenu() {
|
||||
const slot = this.panel.querySelector('slot')!;
|
||||
return slot.assignedElements({ flatten: true }).find(el => el.tagName.toLowerCase() === 'sl-menu') as
|
||||
return this.panel.assignedElements({ flatten: true }).find(el => el.tagName.toLowerCase() === 'sl-menu') as
|
||||
| SlMenu
|
||||
| undefined;
|
||||
}
|
||||
@@ -214,7 +218,8 @@ export default class SlDropdown extends ShoelaceElement {
|
||||
|
||||
handleTriggerKeyDown(event: KeyboardEvent) {
|
||||
// Close when escape or tab is pressed
|
||||
if (event.key === 'Escape') {
|
||||
if (event.key === 'Escape' && this.open) {
|
||||
event.stopPropagation();
|
||||
this.focusOnTrigger();
|
||||
this.hide();
|
||||
return;
|
||||
@@ -261,12 +266,6 @@ export default class SlDropdown extends ShoelaceElement {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Other keys bring focus to the menu and initiate type-to-select behavior
|
||||
const ignoredKeys = ['Tab', 'Shift', 'Meta', 'Ctrl', 'Alt'];
|
||||
if (this.open && !ignoredKeys.includes(event.key)) {
|
||||
menu.typeToSelect(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,8 +291,7 @@ export default class SlDropdown extends ShoelaceElement {
|
||||
// To determine this, we assume the first tabbable element in the trigger slot is the "accessible trigger."
|
||||
//
|
||||
updateAccessibleTrigger() {
|
||||
const slot = this.trigger.querySelector('slot')!;
|
||||
const assignedElements = slot.assignedElements({ flatten: true }) as HTMLElement[];
|
||||
const assignedElements = this.trigger.assignedElements({ flatten: true }) as HTMLElement[];
|
||||
const accessibleTrigger = assignedElements.find(el => getTabbableBoundary(el).start);
|
||||
let target: HTMLElement;
|
||||
|
||||
@@ -414,25 +412,23 @@ export default class SlDropdown extends ShoelaceElement {
|
||||
'dropdown--open': this.open
|
||||
})}
|
||||
>
|
||||
<span
|
||||
<slot
|
||||
name="trigger"
|
||||
slot="anchor"
|
||||
part="trigger"
|
||||
class="dropdown__trigger"
|
||||
@click=${this.handleTriggerClick}
|
||||
@keydown=${this.handleTriggerKeyDown}
|
||||
@keyup=${this.handleTriggerKeyUp}
|
||||
>
|
||||
<slot name="trigger" @slotchange=${this.handleTriggerSlotChange}></slot>
|
||||
</span>
|
||||
@slotchange=${this.handleTriggerSlotChange}
|
||||
></slot>
|
||||
|
||||
<div
|
||||
<slot
|
||||
part="panel"
|
||||
class="dropdown__panel"
|
||||
aria-hidden=${this.open ? 'false' : 'true'}
|
||||
aria-labelledby="dropdown"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
></slot>
|
||||
</sl-popup>
|
||||
`;
|
||||
}
|
||||
@@ -440,16 +436,16 @@ export default class SlDropdown extends ShoelaceElement {
|
||||
|
||||
setDefaultAnimation('dropdown.show', {
|
||||
keyframes: [
|
||||
{ opacity: 0, transform: 'scale(0.9)' },
|
||||
{ opacity: 1, transform: 'scale(1)' }
|
||||
{ opacity: 0, scale: 0.9 },
|
||||
{ opacity: 1, scale: 1 }
|
||||
],
|
||||
options: { duration: 100, easing: 'ease' }
|
||||
});
|
||||
|
||||
setDefaultAnimation('dropdown.hide', {
|
||||
keyframes: [
|
||||
{ opacity: 1, transform: 'scale(1)' },
|
||||
{ opacity: 0, transform: 'scale(0.9)' }
|
||||
{ opacity: 1, scale: 1 },
|
||||
{ opacity: 0, scale: 0.9 }
|
||||
],
|
||||
options: { duration: 100, easing: 'ease' }
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, fixture, html, elementUpdated } from '@open-wc/testing';
|
||||
import { elementUpdated, expect, fixture, html } from '@open-wc/testing';
|
||||
import type SlFormatBytes from './format-bytes';
|
||||
|
||||
describe('<sl-format-bytes>', () => {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
|
||||
/**
|
||||
* @summary Formats a number as a human readable bytes value.
|
||||
*
|
||||
* @since 2.0
|
||||
* @documentation https://shoelace.style/components/format-bytes
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*/
|
||||
@customElement('sl-format-bytes')
|
||||
export default class SlFormatBytes extends ShoelaceElement {
|
||||
@@ -15,7 +15,7 @@ export default class SlFormatBytes extends ShoelaceElement {
|
||||
/** The number to format in bytes. */
|
||||
@property({ type: Number }) value = 0;
|
||||
|
||||
/** The unit to display. */
|
||||
/** The type of unit to display. */
|
||||
@property() unit: 'byte' | 'bit' = 'byte';
|
||||
|
||||
/** Determines how to display the result, e.g. "100 bytes", "100 b", or "100b". */
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { html } from 'lit';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
|
||||
/**
|
||||
* @summary Formats a date/time using the specified locale and options.
|
||||
*
|
||||
* @since 2.0
|
||||
* @documentation https://shoelace.style/components/format-date
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*/
|
||||
@customElement('sl-format-date')
|
||||
export default class SlFormatDate extends ShoelaceElement {
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
/** The date/time to format. If not set, the current date and time will be used. */
|
||||
/**
|
||||
* The date/time to format. If not set, the current date and time will be used. When passing a string, it's strongly
|
||||
* recommended to use the ISO 8601 format to ensure timezones are handled correctly. To convert a date to this format
|
||||
* in JavaScript, use [`date.toISOString()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString).
|
||||
*/
|
||||
@property() date: Date | string = new Date();
|
||||
|
||||
/** The format for displaying the weekday. */
|
||||
@@ -46,7 +50,7 @@ export default class SlFormatDate extends ShoelaceElement {
|
||||
/** The time zone to express the time in. */
|
||||
@property({ attribute: 'time-zone' }) timeZone: string;
|
||||
|
||||
/** When set, 24 hour time will always be used. */
|
||||
/** The format for displaying the hour. */
|
||||
@property({ attribute: 'hour-format' }) hourFormat: 'auto' | '12' | '24' = 'auto';
|
||||
|
||||
render() {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
|
||||
/**
|
||||
* @summary Formats a number using the specified locale and options.
|
||||
*
|
||||
* @since 2.0
|
||||
* @documentation https://shoelace.style/components/format-number
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*/
|
||||
@customElement('sl-format-number')
|
||||
export default class SlFormatNumber extends ShoelaceElement {
|
||||
@@ -21,25 +21,25 @@ export default class SlFormatNumber extends ShoelaceElement {
|
||||
/** Turns off grouping separators. */
|
||||
@property({ attribute: 'no-grouping', type: Boolean }) noGrouping = false;
|
||||
|
||||
/** The currency to use when formatting. Must be an ISO 4217 currency code such as `USD` or `EUR`. */
|
||||
/** The [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217) currency code to use when formatting. */
|
||||
@property() currency = 'USD';
|
||||
|
||||
/** How to display the currency. */
|
||||
@property({ attribute: 'currency-display' }) currencyDisplay: 'symbol' | 'narrowSymbol' | 'code' | 'name' = 'symbol';
|
||||
|
||||
/** The minimum number of integer digits to use. Possible values are 1 - 21. */
|
||||
/** The minimum number of integer digits to use. Possible values are 1-21. */
|
||||
@property({ attribute: 'minimum-integer-digits', type: Number }) minimumIntegerDigits: number;
|
||||
|
||||
/** The minimum number of fraction digits to use. Possible values are 0 - 20. */
|
||||
/** The minimum number of fraction digits to use. Possible values are 0-20. */
|
||||
@property({ attribute: 'minimum-fraction-digits', type: Number }) minimumFractionDigits: number;
|
||||
|
||||
/** The maximum number of fraction digits to use. Possible values are 0 - 20. */
|
||||
/** The maximum number of fraction digits to use. Possible values are 0-0. */
|
||||
@property({ attribute: 'maximum-fraction-digits', type: Number }) maximumFractionDigits: number;
|
||||
|
||||
/** The minimum number of significant digits to use. Possible values are 1 - 21. */
|
||||
/** The minimum number of significant digits to use. Possible values are 1-21. */
|
||||
@property({ attribute: 'minimum-significant-digits', type: Number }) minimumSignificantDigits: number;
|
||||
|
||||
/** The maximum number of significant digits to use,. Possible values are 1 - 21. */
|
||||
/** The maximum number of significant digits to use,. Possible values are 1-21. */
|
||||
@property({ attribute: 'maximum-significant-digits', type: Number }) maximumSignificantDigits: number;
|
||||
|
||||
render() {
|
||||
|
||||
@@ -20,12 +20,12 @@ export default css`
|
||||
color: inherit;
|
||||
padding: var(--sl-spacing-x-small);
|
||||
cursor: pointer;
|
||||
transition: var(--sl-transition-medium) color;
|
||||
transition: var(--sl-transition-x-fast) color;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.icon-button:hover:not(.icon-button--disabled),
|
||||
.icon-button:focus:not(.icon-button--disabled) {
|
||||
.icon-button:focus-visible:not(.icon-button--disabled) {
|
||||
color: var(--sl-color-primary-600);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,40 +1,43 @@
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { html, literal } from 'lit/static-html.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import '../icon/icon';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { html, literal } from 'lit/static-html.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import styles from './icon-button.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
* @summary Icons buttons are simple, icon-only buttons that can be used for actions and in toolbars.
|
||||
*
|
||||
* @since 2.0
|
||||
* @documentation https://shoelace.style/components/icon-button
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @dependency sl-icon
|
||||
*
|
||||
* @event sl-blur - Emitted when the icon button loses focus.
|
||||
* @event sl-focus - Emitted when the icon button gains focus.
|
||||
*
|
||||
* @csspart base - The component's internal wrapper.
|
||||
* @csspart base - The component's base wrapper.
|
||||
*/
|
||||
@customElement('sl-icon-button')
|
||||
export default class SlIconButton extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
@state() private hasFocus = false;
|
||||
|
||||
@query('.icon-button') button: HTMLButtonElement | HTMLLinkElement;
|
||||
|
||||
/** The name of the icon to draw. */
|
||||
@state() private hasFocus = false;
|
||||
|
||||
/** The name of the icon to draw. Available names depend on the icon library being used. */
|
||||
@property() name?: string;
|
||||
|
||||
/** The name of a registered custom icon library. */
|
||||
@property() library?: string;
|
||||
|
||||
/** An external URL of an SVG file. */
|
||||
/**
|
||||
* An external URL of an SVG file. Be sure you trust the content you are including, as it will be executed as code and
|
||||
* can result in XSS attacks.
|
||||
*/
|
||||
@property() src?: string;
|
||||
|
||||
/** When set, the underlying button will be rendered as an `<a>` with this `href` instead of a `<button>`. */
|
||||
@@ -47,14 +50,31 @@ export default class SlIconButton extends ShoelaceElement {
|
||||
@property() download?: string;
|
||||
|
||||
/**
|
||||
* A description that gets read by screen readers and other assistive devices. For optimal accessibility, you should
|
||||
* always include a label that describes what the icon button does.
|
||||
* A description that gets read by assistive devices. For optimal accessibility, you should always include a label
|
||||
* that describes what the icon button does.
|
||||
*/
|
||||
@property() label = '';
|
||||
|
||||
/** Disables the button. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
private handleBlur() {
|
||||
this.hasFocus = false;
|
||||
this.emit('sl-blur');
|
||||
}
|
||||
|
||||
private handleFocus() {
|
||||
this.hasFocus = true;
|
||||
this.emit('sl-focus');
|
||||
}
|
||||
|
||||
private handleClick(event: MouseEvent) {
|
||||
if (this.disabled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
/** Simulates a click on the icon button. */
|
||||
click() {
|
||||
this.button.click();
|
||||
@@ -70,23 +90,6 @@ export default class SlIconButton extends ShoelaceElement {
|
||||
this.button.blur();
|
||||
}
|
||||
|
||||
handleBlur() {
|
||||
this.hasFocus = false;
|
||||
this.emit('sl-blur');
|
||||
}
|
||||
|
||||
handleFocus() {
|
||||
this.hasFocus = true;
|
||||
this.emit('sl-focus');
|
||||
}
|
||||
|
||||
handleClick(event: MouseEvent) {
|
||||
if (this.disabled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const isLink = this.href ? true : false;
|
||||
const tag = isLink ? literal`a` : literal`button`;
|
||||
|
||||
@@ -8,7 +8,6 @@ export default css`
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
contain: strict;
|
||||
box-sizing: content-box !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { watch } from '../../internal/watch';
|
||||
import styles from './icon.styles';
|
||||
import { getIconLibrary, unwatchIcon, watchIcon } from './library';
|
||||
import { html } from 'lit';
|
||||
import { requestIcon } from './request';
|
||||
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
||||
import { watch } from '../../internal/watch';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import styles from './icon.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
let parser: DOMParser;
|
||||
|
||||
/**
|
||||
* @summary Icons are symbols that can be used to represent various options within an application.
|
||||
*
|
||||
* @since 2.0
|
||||
* @documentation https://shoelace.style/components/icon
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @event sl-load - Emitted when the icon has loaded.
|
||||
* @event sl-error - Emitted when the icon fails to load due to an error.
|
||||
@@ -25,17 +25,19 @@ export default class SlIcon extends ShoelaceElement {
|
||||
|
||||
@state() private svg = '';
|
||||
|
||||
/** The name of the icon to draw. */
|
||||
/** The name of the icon to draw. Available names depend on the icon library being used. */
|
||||
@property({ reflect: true }) name?: string;
|
||||
|
||||
/**
|
||||
* An external URL of an SVG file.
|
||||
*
|
||||
* WARNING: Be sure you trust the content you are including as it will be executed as code and can result in XSS attacks.
|
||||
* An external URL of an SVG file. Be sure you trust the content you are including, as it will be executed as code and
|
||||
* can result in XSS attacks.
|
||||
*/
|
||||
@property() src?: string;
|
||||
|
||||
/** An alternate description to use for accessibility. If omitted, the icon will be ignored by assistive devices. */
|
||||
/**
|
||||
* An alternate description to use for assistive devices. If omitted, the icon will be considered presentational and
|
||||
* ignored by assistive devices.
|
||||
*/
|
||||
@property() label = '';
|
||||
|
||||
/** The name of a registered custom icon library. */
|
||||
@@ -63,11 +65,6 @@ export default class SlIcon extends ShoelaceElement {
|
||||
return this.src;
|
||||
}
|
||||
|
||||
/** @internal Fetches the icon and redraws it. Used to handle library registrations. */
|
||||
redraw() {
|
||||
this.setIcon();
|
||||
}
|
||||
|
||||
@watch('label')
|
||||
handleLabelChange() {
|
||||
const hasLabel = typeof this.label === 'string' && this.label.length > 0;
|
||||
@@ -83,9 +80,7 @@ export default class SlIcon extends ShoelaceElement {
|
||||
}
|
||||
}
|
||||
|
||||
@watch('name')
|
||||
@watch('src')
|
||||
@watch('library')
|
||||
@watch(['name', 'src', 'library'])
|
||||
async setIcon() {
|
||||
const library = getIconLibrary(this.library);
|
||||
const url = this.getUrl();
|
||||
@@ -127,10 +122,6 @@ export default class SlIcon extends ShoelaceElement {
|
||||
}
|
||||
}
|
||||
|
||||
handleChange() {
|
||||
this.setIcon();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` ${unsafeSVG(this.svg)} `;
|
||||
}
|
||||
|
||||
@@ -103,9 +103,9 @@ const icons = {
|
||||
<path d="M3.612 15.443c-.386.198-.824-.149-.746-.592l.83-4.73L.173 6.765c-.329-.314-.158-.888.283-.95l4.898-.696L7.538.792c.197-.39.73-.39.927 0l2.184 4.327 4.898.696c.441.062.612.636.282.95l-3.522 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256z"/>
|
||||
</svg>
|
||||
`,
|
||||
x: `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-x" viewBox="0 0 16 16">
|
||||
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
|
||||
'x-lg': `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-x-lg" viewBox="0 0 16 16">
|
||||
<path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854Z"/>
|
||||
</svg>
|
||||
`,
|
||||
'x-circle-fill': `
|
||||
|
||||
@@ -13,18 +13,22 @@ export interface IconLibrary {
|
||||
let registry: IconLibrary[] = [defaultLibrary, systemLibrary];
|
||||
let watchedIcons: SlIcon[] = [];
|
||||
|
||||
/** Adds an icon to the list of watched icons. */
|
||||
export function watchIcon(icon: SlIcon) {
|
||||
watchedIcons.push(icon);
|
||||
}
|
||||
|
||||
/** Removes an icon from the list of watched icons. */
|
||||
export function unwatchIcon(icon: SlIcon) {
|
||||
watchedIcons = watchedIcons.filter(el => el !== icon);
|
||||
}
|
||||
|
||||
/** Returns a library from the registry. */
|
||||
export function getIconLibrary(name?: string) {
|
||||
return registry.find(lib => lib.name === name);
|
||||
}
|
||||
|
||||
/** Adds an icon library to the registry, or overrides an existing one. */
|
||||
export function registerIconLibrary(
|
||||
name: string,
|
||||
options: { resolver: IconLibraryResolver; mutator?: IconLibraryMutator }
|
||||
@@ -39,11 +43,12 @@ export function registerIconLibrary(
|
||||
// Redraw watched icons
|
||||
watchedIcons.forEach(icon => {
|
||||
if (icon.library === name) {
|
||||
icon.redraw();
|
||||
icon.setIcon();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Removes an icon library from the registry. */
|
||||
export function unregisterIconLibrary(name: string) {
|
||||
registry = registry.filter(lib => lib.name !== name);
|
||||
}
|
||||
|
||||
@@ -20,13 +20,14 @@ export default css`
|
||||
|
||||
.image-comparer__before,
|
||||
.image-comparer__after {
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.image-comparer__before ::slotted(img),
|
||||
.image-comparer__after ::slotted(img),
|
||||
.image-comparer__before ::slotted(svg),
|
||||
.image-comparer__after ::slotted(svg) {
|
||||
.image-comparer__before::slotted(img),
|
||||
.image-comparer__after::slotted(img),
|
||||
.image-comparer__before::slotted(svg),
|
||||
.image-comparer__after::slotted(svg) {
|
||||
display: block;
|
||||
max-width: 100% !important;
|
||||
height: auto;
|
||||
@@ -49,7 +50,7 @@ export default css`
|
||||
width: var(--divider-width);
|
||||
height: 100%;
|
||||
background-color: var(--sl-color-neutral-0);
|
||||
transform: translateX(calc(var(--divider-width) / -2));
|
||||
translate: calc(var(--divider-width) / -2);
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ describe('<sl-image-comparer>', () => {
|
||||
`);
|
||||
|
||||
const afterPart = el.shadowRoot!.querySelector<HTMLElement>('[part~="after"]')!;
|
||||
const iconContainer = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name="handle-icon"]')!;
|
||||
const iconContainer = el.shadowRoot!.querySelector<HTMLSlotElement>('slot[name="handle"]')!;
|
||||
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="handle"]')!;
|
||||
|
||||
expect(el.position).to.equal(50);
|
||||
|
||||
@@ -1,36 +1,35 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { drag } from '../../internal/drag';
|
||||
import { clamp } from '../../internal/math';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import '../icon/icon';
|
||||
import { clamp } from '../../internal/math';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { drag } from '../../internal/drag';
|
||||
import { html } from 'lit';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { watch } from '../../internal/watch';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import styles from './image-comparer.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
* @summary Compare visual differences between similar photos with a sliding panel.
|
||||
*
|
||||
* @since 2.0
|
||||
* @documentation https://shoelace.style/components/image-comparer
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @dependency sl-icon
|
||||
*
|
||||
* @slot before - The before image, an `<img>` or `<svg>` element.
|
||||
* @slot after - The after image, an `<img>` or `<svg>` element.
|
||||
* @slot handle-icon - The icon used inside the handle.
|
||||
* @slot handle - The icon used inside the handle.
|
||||
*
|
||||
* @event sl-change - Emitted when the position changes.
|
||||
*
|
||||
* @csspart base - The component's internal wrapper.
|
||||
* @csspart before - The container that holds the "before" image.
|
||||
* @csspart after - The container that holds the "after" image.
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart before - The container that wraps the before image.
|
||||
* @csspart after - The container that wraps the after image.
|
||||
* @csspart divider - The divider that separates the images.
|
||||
* @csspart handle - The handle that the user drags to expose the after image.
|
||||
* @csspart handle-icon - The drag icon that appears inside the handle.
|
||||
*
|
||||
* @cssproperty --divider-width - The width of the dividing line.
|
||||
* @cssproperty --handle-size - The size of the compare handle.
|
||||
@@ -39,15 +38,15 @@ import type { CSSResultGroup } from 'lit';
|
||||
export default class SlImageComparer extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
@query('.image-comparer') base: HTMLElement;
|
||||
@query('.image-comparer__handle') handle: HTMLElement;
|
||||
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
/** The position of the divider as a percentage. */
|
||||
@property({ type: Number, reflect: true }) position = 50;
|
||||
|
||||
handleDrag(event: PointerEvent) {
|
||||
private handleDrag(event: PointerEvent) {
|
||||
const { width } = this.base.getBoundingClientRect();
|
||||
const isRtl = this.localize.dir() === 'rtl';
|
||||
|
||||
@@ -62,7 +61,7 @@ export default class SlImageComparer extends ShoelaceElement {
|
||||
});
|
||||
}
|
||||
|
||||
handleKeyDown(event: KeyboardEvent) {
|
||||
private handleKeyDown(event: KeyboardEvent) {
|
||||
const isLtr = this.localize.dir() === 'ltr';
|
||||
const isRtl = this.localize.dir() === 'rtl';
|
||||
|
||||
@@ -109,19 +108,16 @@ export default class SlImageComparer extends ShoelaceElement {
|
||||
@keydown=${this.handleKeyDown}
|
||||
>
|
||||
<div class="image-comparer__image">
|
||||
<div part="before" class="image-comparer__before">
|
||||
<slot name="before"></slot>
|
||||
</div>
|
||||
<slot name="before" part="before" class="image-comparer__before"></slot>
|
||||
|
||||
<div
|
||||
<slot
|
||||
name="after"
|
||||
part="after"
|
||||
class="image-comparer__after"
|
||||
style=${styleMap({
|
||||
clipPath: isRtl ? `inset(0 0 0 ${100 - this.position}%)` : `inset(0 ${100 - this.position}% 0 0)`
|
||||
})}
|
||||
>
|
||||
<slot name="after"></slot>
|
||||
</div>
|
||||
></slot>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -133,7 +129,8 @@ export default class SlImageComparer extends ShoelaceElement {
|
||||
@mousedown=${this.handleDrag}
|
||||
@touchstart=${this.handleDrag}
|
||||
>
|
||||
<div
|
||||
<slot
|
||||
name="handle"
|
||||
part="handle"
|
||||
class="image-comparer__handle"
|
||||
role="scrollbar"
|
||||
@@ -143,10 +140,8 @@ export default class SlImageComparer extends ShoelaceElement {
|
||||
aria-controls="image-comparer"
|
||||
tabindex="0"
|
||||
>
|
||||
<slot name="handle-icon">
|
||||
<sl-icon part="handle-icon" library="system" name="grip-vertical"></sl-icon>
|
||||
</slot>
|
||||
</div>
|
||||
<sl-icon library="system" name="grip-vertical"></sl-icon>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, fixture, html, waitUntil, aTimeout } from '@open-wc/testing';
|
||||
import { aTimeout, expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
import type SlInclude from './include';
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { watch } from '../../internal/watch';
|
||||
import styles from './include.styles';
|
||||
import { html } from 'lit';
|
||||
import { requestInclude } from './request';
|
||||
import { watch } from '../../internal/watch';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import styles from './include.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
* @summary Includes give you the power to embed external HTML files into the page.
|
||||
*
|
||||
* @since 2.0
|
||||
* @documentation https://shoelace.style/components/include
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @event sl-load - Emitted when the included file is loaded.
|
||||
* @event {{ status: number }} sl-error - Emitted when the included file fails to load due to an error.
|
||||
@@ -20,9 +20,8 @@ export default class SlInclude extends ShoelaceElement {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
/**
|
||||
* The location of the HTML file to include.
|
||||
*
|
||||
* WARNING: Be sure you trust the content you are including as it will be executed as code and can result in XSS attacks.
|
||||
* The location of the HTML file to include. Be sure you trust the content you are including as it will be executed as
|
||||
* code and can result in XSS attacks.
|
||||
*/
|
||||
@property() src: string;
|
||||
|
||||
@@ -30,12 +29,12 @@ export default class SlInclude extends ShoelaceElement {
|
||||
@property() mode: 'cors' | 'no-cors' | 'same-origin' = 'cors';
|
||||
|
||||
/**
|
||||
* Allows included scripts to be executed. You must ensure the content you're including is trusted, otherwise this
|
||||
* option can lead to XSS vulnerabilities in your app!
|
||||
* Allows included scripts to be executed. Be sure you trust the content you are including as it will be executed as
|
||||
* code and can result in XSS attacks.
|
||||
*/
|
||||
@property({ attribute: 'allow-scripts', type: Boolean }) allowScripts = false;
|
||||
|
||||
executeScript(script: HTMLScriptElement) {
|
||||
private executeScript(script: HTMLScriptElement) {
|
||||
// Create a copy of the script and swap it out so the browser executes it
|
||||
const newScript = document.createElement('script');
|
||||
[...script.attributes].forEach(attr => newScript.setAttribute(attr.name, attr.value));
|
||||
|
||||
@@ -6,6 +6,7 @@ interface IncludeFile {
|
||||
|
||||
const includeFiles = new Map<string, Promise<IncludeFile>>();
|
||||
|
||||
/** Fetches an include file from a remote source. Caching is enabled so the origin is only pinged once. */
|
||||
export function requestInclude(src: string, mode: 'cors' | 'no-cors' | 'same-origin' = 'cors'): Promise<IncludeFile> {
|
||||
if (includeFiles.has(src)) {
|
||||
return includeFiles.get(src)!;
|
||||
|
||||
@@ -147,8 +147,8 @@ export default css`
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.input__prefix ::slotted(sl-icon),
|
||||
.input__suffix ::slotted(sl-icon) {
|
||||
.input__prefix::slotted(sl-icon),
|
||||
.input__suffix::slotted(sl-icon) {
|
||||
color: var(--sl-input-icon-color);
|
||||
}
|
||||
|
||||
@@ -172,11 +172,11 @@ export default css`
|
||||
width: calc(1em + var(--sl-input-spacing-small) * 2);
|
||||
}
|
||||
|
||||
.input--small .input__prefix ::slotted(*) {
|
||||
.input--small .input__prefix::slotted(*) {
|
||||
margin-inline-start: var(--sl-input-spacing-small);
|
||||
}
|
||||
|
||||
.input--small .input__suffix ::slotted(*) {
|
||||
.input--small .input__suffix::slotted(*) {
|
||||
margin-inline-end: var(--sl-input-spacing-small);
|
||||
}
|
||||
|
||||
@@ -196,11 +196,11 @@ export default css`
|
||||
width: calc(1em + var(--sl-input-spacing-medium) * 2);
|
||||
}
|
||||
|
||||
.input--medium .input__prefix ::slotted(*) {
|
||||
.input--medium .input__prefix::slotted(*) {
|
||||
margin-inline-start: var(--sl-input-spacing-medium);
|
||||
}
|
||||
|
||||
.input--medium .input__suffix ::slotted(*) {
|
||||
.input--medium .input__suffix::slotted(*) {
|
||||
margin-inline-end: var(--sl-input-spacing-medium);
|
||||
}
|
||||
|
||||
@@ -220,11 +220,11 @@ export default css`
|
||||
width: calc(1em + var(--sl-input-spacing-large) * 2);
|
||||
}
|
||||
|
||||
.input--large .input__prefix ::slotted(*) {
|
||||
.input--large .input__prefix::slotted(*) {
|
||||
margin-inline-start: var(--sl-input-spacing-large);
|
||||
}
|
||||
|
||||
.input--large .input__suffix ::slotted(*) {
|
||||
.input--large .input__suffix::slotted(*) {
|
||||
margin-inline-end: var(--sl-input-spacing-large);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment
|
||||
import { expect, fixture, html, oneEvent, waitUntil } from '@open-wc/testing';
|
||||
import { getFormControls } from '../../../dist/utilities/form.js';
|
||||
import { sendKeys } from '@web/test-runner-commands';
|
||||
import { serialize } from '../../utilities/form'; // must come from the same module
|
||||
import sinon from 'sinon';
|
||||
import { serialize } from '../../utilities/form';
|
||||
import type SlInput from './input';
|
||||
|
||||
describe('<sl-input>', () => {
|
||||
@@ -18,6 +20,7 @@ describe('<sl-input>', () => {
|
||||
expect(el.name).to.equal('');
|
||||
expect(el.value).to.equal('');
|
||||
expect(el.defaultValue).to.equal('');
|
||||
expect(el.title).to.equal('');
|
||||
expect(el.filled).to.be.false;
|
||||
expect(el.pill).to.be.false;
|
||||
expect(el.label).to.equal('');
|
||||
@@ -41,12 +44,19 @@ describe('<sl-input>', () => {
|
||||
expect(el.autocomplete).to.be.undefined;
|
||||
expect(el.autofocus).to.be.undefined;
|
||||
expect(el.enterkeyhint).to.be.undefined;
|
||||
expect(el.spellcheck).to.be.undefined;
|
||||
expect(el.spellcheck).to.be.true;
|
||||
expect(el.inputmode).to.be.undefined;
|
||||
expect(el.valueAsDate).to.be.null;
|
||||
expect(isNaN(el.valueAsNumber)).to.be.true;
|
||||
});
|
||||
|
||||
it('should have title if title attribute is set', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input title="Test"></sl-input> `);
|
||||
const input = el.shadowRoot!.querySelector<HTMLInputElement>('[part~="input"]')!;
|
||||
|
||||
expect(input.title).to.equal('Test');
|
||||
});
|
||||
|
||||
it('should be disabled with the disabled attribute', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input disabled></sl-input> `);
|
||||
const input = el.shadowRoot!.querySelector<HTMLInputElement>('[part~="input"]')!;
|
||||
@@ -77,42 +87,77 @@ describe('<sl-input>', () => {
|
||||
it('should focus the input when clicking on the label', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input label="Name"></sl-input> `);
|
||||
const label = el.shadowRoot!.querySelector('[part~="form-control-label"]')!;
|
||||
const submitHandler = sinon.spy();
|
||||
const focusHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-focus', submitHandler);
|
||||
el.addEventListener('sl-focus', focusHandler);
|
||||
(label as HTMLLabelElement).click();
|
||||
await waitUntil(() => submitHandler.calledOnce);
|
||||
await waitUntil(() => focusHandler.calledOnce);
|
||||
|
||||
expect(submitHandler).to.have.been.calledOnce;
|
||||
expect(focusHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
describe('when using constraint validation', () => {
|
||||
it('should be valid by default', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input></sl-input> `);
|
||||
expect(el.invalid).to.be.false;
|
||||
expect(el.checkValidity()).to.be.true;
|
||||
});
|
||||
|
||||
it('should be invalid when required and empty', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input required></sl-input> `);
|
||||
expect(el.reportValidity()).to.be.false;
|
||||
expect(el.invalid).to.be.true;
|
||||
});
|
||||
|
||||
it('should be invalid when the pattern does not match', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input pattern="^test" value="fail"></sl-input> `);
|
||||
expect(el.invalid).to.be.true;
|
||||
expect(el.reportValidity()).to.be.false;
|
||||
expect(el.checkValidity()).to.be.false;
|
||||
});
|
||||
|
||||
it('should be invalid when required and disabled is removed', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input disabled required></sl-input> `);
|
||||
el.disabled = false;
|
||||
await el.updateComplete;
|
||||
expect(el.invalid).to.be.true;
|
||||
expect(el.checkValidity()).to.be.false;
|
||||
});
|
||||
|
||||
it('should receive the correct validation attributes ("states") when valid', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input required value="a"></sl-input> `);
|
||||
|
||||
expect(el.checkValidity()).to.be.true;
|
||||
expect(el.hasAttribute('data-required')).to.be.true;
|
||||
expect(el.hasAttribute('data-optional')).to.be.false;
|
||||
expect(el.hasAttribute('data-invalid')).to.be.false;
|
||||
expect(el.hasAttribute('data-valid')).to.be.true;
|
||||
expect(el.hasAttribute('data-user-invalid')).to.be.false;
|
||||
expect(el.hasAttribute('data-user-valid')).to.be.false;
|
||||
|
||||
el.focus();
|
||||
await el.updateComplete;
|
||||
await sendKeys({ press: 'b' });
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.checkValidity()).to.be.true;
|
||||
expect(el.hasAttribute('data-user-invalid')).to.be.false;
|
||||
expect(el.hasAttribute('data-user-valid')).to.be.true;
|
||||
});
|
||||
|
||||
it('should receive the correct validation attributes ("states") when invalid', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input required></sl-input> `);
|
||||
|
||||
expect(el.hasAttribute('data-required')).to.be.true;
|
||||
expect(el.hasAttribute('data-optional')).to.be.false;
|
||||
expect(el.hasAttribute('data-invalid')).to.be.true;
|
||||
expect(el.hasAttribute('data-valid')).to.be.false;
|
||||
expect(el.hasAttribute('data-user-invalid')).to.be.false;
|
||||
expect(el.hasAttribute('data-user-valid')).to.be.false;
|
||||
|
||||
el.focus();
|
||||
await el.updateComplete;
|
||||
await sendKeys({ press: 'a' });
|
||||
await sendKeys({ press: 'Backspace' });
|
||||
await el.updateComplete;
|
||||
|
||||
expect(el.hasAttribute('data-user-invalid')).to.be.true;
|
||||
expect(el.hasAttribute('data-user-valid')).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('when serializing', () => {
|
||||
describe('when submitting a form', () => {
|
||||
it('should serialize its name and value with FormData', async () => {
|
||||
const form = await fixture<HTMLFormElement>(html` <form><sl-input name="a" value="1"></sl-input></form> `);
|
||||
const formData = new FormData(form);
|
||||
@@ -124,9 +169,7 @@ describe('<sl-input>', () => {
|
||||
const json = serialize(form);
|
||||
expect(json.a).to.equal('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when submitting a form', () => {
|
||||
it('should submit the form when pressing enter in a form without a submit button', async () => {
|
||||
const form = await fixture<HTMLFormElement>(html` <form><sl-input></sl-input></form> `);
|
||||
const input = form.querySelector('sl-input')!;
|
||||
@@ -159,6 +202,41 @@ describe('<sl-input>', () => {
|
||||
expect(keydownHandler).to.have.been.calledOnce;
|
||||
expect(submitHandler).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('should be invalid when setCustomValidity() is called with a non-empty value', async () => {
|
||||
const input = await fixture<HTMLFormElement>(html` <sl-input></sl-input> `);
|
||||
|
||||
input.setCustomValidity('Invalid selection');
|
||||
await input.updateComplete;
|
||||
|
||||
expect(input.checkValidity()).to.be.false;
|
||||
expect(input.hasAttribute('data-invalid')).to.be.true;
|
||||
expect(input.hasAttribute('data-valid')).to.be.false;
|
||||
expect(input.hasAttribute('data-user-invalid')).to.be.false;
|
||||
expect(input.hasAttribute('data-user-valid')).to.be.false;
|
||||
|
||||
input.focus();
|
||||
await sendKeys({ type: 'test' });
|
||||
await input.updateComplete;
|
||||
|
||||
expect(input.hasAttribute('data-user-invalid')).to.be.true;
|
||||
expect(input.hasAttribute('data-user-valid')).to.be.false;
|
||||
});
|
||||
|
||||
it('should be present in form data when using the form attribute and located outside of a <form>', async () => {
|
||||
const el = await fixture<HTMLFormElement>(html`
|
||||
<div>
|
||||
<form id="f">
|
||||
<sl-button type="submit">Submit</sl-button>
|
||||
</form>
|
||||
<sl-input form="f" name="a" value="1"></sl-input>
|
||||
</div>
|
||||
`);
|
||||
const form = el.querySelector('form')!;
|
||||
const formData = new FormData(form);
|
||||
|
||||
expect(formData.get('a')).to.equal('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when resetting a form', () => {
|
||||
@@ -226,27 +304,67 @@ describe('<sl-input>', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the value changes', () => {
|
||||
it('should emit sl-change and sl-input when the user types in the input', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input></sl-input> `);
|
||||
const inputHandler = sinon.spy();
|
||||
const changeHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.focus();
|
||||
await sendKeys({ type: 'abc' });
|
||||
el.blur();
|
||||
await el.updateComplete;
|
||||
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
expect(inputHandler).to.have.been.calledThrice;
|
||||
});
|
||||
|
||||
it('should not emit sl-change or sl-input when the value is set programmatically', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input></sl-input> `);
|
||||
|
||||
el.addEventListener('sl-change', () => expect.fail('sl-change should not be emitted'));
|
||||
el.addEventListener('sl-input', () => expect.fail('sl-input should not be emitted'));
|
||||
el.value = 'abc';
|
||||
|
||||
await el.updateComplete;
|
||||
});
|
||||
|
||||
it('should not emit sl-change or sl-input when calling setinputText()', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input value="hi there"></sl-input> `);
|
||||
|
||||
el.addEventListener('sl-change', () => expect.fail('sl-change should not be emitted'));
|
||||
el.addEventListener('sl-input', () => expect.fail('sl-input should not be emitted'));
|
||||
el.focus();
|
||||
el.setSelectionRange(0, 2);
|
||||
el.setRangeText('hello');
|
||||
|
||||
await el.updateComplete;
|
||||
});
|
||||
});
|
||||
|
||||
describe('when type="number"', () => {
|
||||
it('should be valid when the value is within the boundary of a step', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input type="number" step=".5" value="1.5"></sl-input> `);
|
||||
expect(el.invalid).to.be.false;
|
||||
expect(el.checkValidity()).to.be.true;
|
||||
});
|
||||
|
||||
it('should be invalid when the value is not within the boundary of a step', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input type="number" step=".5" value="1.25"></sl-input> `);
|
||||
expect(el.invalid).to.be.true;
|
||||
expect(el.checkValidity()).to.be.false;
|
||||
});
|
||||
|
||||
it('should update validity when step changes', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input type="number" step=".5" value="1.5"></sl-input> `);
|
||||
expect(el.invalid).to.be.false;
|
||||
expect(el.checkValidity()).to.be.true;
|
||||
|
||||
el.step = 1;
|
||||
await el.updateComplete;
|
||||
expect(el.invalid).to.be.true;
|
||||
expect(el.checkValidity()).to.be.false;
|
||||
});
|
||||
|
||||
it('should increment by step if stepUp() is called', async () => {
|
||||
it('should increment by step when stepUp() is called', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input type="number" step="2" value="2"></sl-input> `);
|
||||
|
||||
el.stepUp();
|
||||
@@ -254,7 +372,7 @@ describe('<sl-input>', () => {
|
||||
expect(el.value).to.equal('4');
|
||||
});
|
||||
|
||||
it('should decrement by step if stepDown() is called', async () => {
|
||||
it('should decrement by step when stepDown() is called', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input type="number" step="2" value="2"></sl-input> `);
|
||||
|
||||
el.stepDown();
|
||||
@@ -262,35 +380,102 @@ describe('<sl-input>', () => {
|
||||
expect(el.value).to.equal('0');
|
||||
});
|
||||
|
||||
it('should fire sl-input and sl-change if stepUp() is called', async () => {
|
||||
it('should not emit sl-input or sl-change when stepUp() is called programmatically', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input type="number" step="2" value="2"></sl-input> `);
|
||||
|
||||
const inputHandler = sinon.spy();
|
||||
const changeHandler = sinon.spy();
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
|
||||
el.addEventListener('sl-change', () => expect.fail('sl-change should not be emitted'));
|
||||
el.addEventListener('sl-input', () => expect.fail('sl-input should not be emitted'));
|
||||
el.stepUp();
|
||||
|
||||
await waitUntil(() => inputHandler.calledOnce);
|
||||
await waitUntil(() => changeHandler.calledOnce);
|
||||
expect(inputHandler).to.have.been.calledOnce;
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
await el.updateComplete;
|
||||
});
|
||||
|
||||
it('should fire sl-input and sl-change if stepDown() is called', async () => {
|
||||
it('should not emit sl-input and sl-change when stepDown() is called programmatically', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input type="number" step="2" value="2"></sl-input> `);
|
||||
|
||||
const inputHandler = sinon.spy();
|
||||
const changeHandler = sinon.spy();
|
||||
el.addEventListener('sl-input', inputHandler);
|
||||
el.addEventListener('sl-change', changeHandler);
|
||||
el.addEventListener('sl-change', () => expect.fail('sl-change should not be emitted'));
|
||||
el.addEventListener('sl-input', () => expect.fail('sl-input should not be emitted'));
|
||||
el.stepDown();
|
||||
|
||||
el.stepUp();
|
||||
await el.updateComplete;
|
||||
});
|
||||
});
|
||||
|
||||
await waitUntil(() => inputHandler.calledOnce);
|
||||
await waitUntil(() => changeHandler.calledOnce);
|
||||
expect(changeHandler).to.have.been.calledOnce;
|
||||
describe('when using spellcheck', () => {
|
||||
it('should enable spellcheck when no attribute is present', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input></sl-input> `);
|
||||
const input = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
|
||||
expect(input.getAttribute('spellcheck')).to.equal('true');
|
||||
expect(input.spellcheck).to.be.true;
|
||||
});
|
||||
|
||||
it('should enable spellcheck when set to "true"', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input spellcheck="true"></sl-input> `);
|
||||
const input = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
|
||||
expect(input.getAttribute('spellcheck')).to.equal('true');
|
||||
expect(input.spellcheck).to.be.true;
|
||||
});
|
||||
|
||||
it('should disable spellcheck when set to "false"', async () => {
|
||||
const el = await fixture<SlInput>(html` <sl-input spellcheck="false"></sl-input> `);
|
||||
const input = el.shadowRoot!.querySelector<HTMLInputElement>('input')!;
|
||||
expect(input.getAttribute('spellcheck')).to.equal('false');
|
||||
expect(input.spellcheck).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using FormControlController', () => {
|
||||
it('should submit with the correct form when the form attribute changes', async () => {
|
||||
const el = await fixture<HTMLFormElement>(html`
|
||||
<div>
|
||||
<form id="f1">
|
||||
<input type="hidden" name="b" value="2" />
|
||||
<sl-button type="submit">Submit</sl-button>
|
||||
</form>
|
||||
<form id="f2">
|
||||
<input type="hidden" name="c" value="3" />
|
||||
<sl-button type="submit">Submit</sl-button>
|
||||
</form>
|
||||
<sl-input form="f1" name="a" value="1"></sl-input>
|
||||
</div>
|
||||
`);
|
||||
const form = el.querySelector<HTMLFormElement>('#f2')!;
|
||||
const input = document.querySelector('sl-input')!;
|
||||
|
||||
input.form = 'f2';
|
||||
await input.updateComplete;
|
||||
|
||||
const formData = new FormData(form);
|
||||
|
||||
expect(formData.get('a')).to.equal('1');
|
||||
expect(formData.get('b')).to.be.null;
|
||||
expect(formData.get('c')).to.equal('3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using the getFormControls() function', () => {
|
||||
it('should return both native and Shoelace form controls in the correct DOM order', async () => {
|
||||
const el = await fixture<HTMLFormElement>(html`
|
||||
<div>
|
||||
<input type="text" name="a" value="1" form="f1" />
|
||||
<sl-input type="text" name="b" value="2" form="f1"></sl-input>
|
||||
<form id="f1">
|
||||
<input type="hidden" name="c" value="3" />
|
||||
<input type="text" name="d" value="4" />
|
||||
<sl-input name="e" value="5"></sl-input>
|
||||
<textarea name="f">6</textarea>
|
||||
<sl-textarea name="g" value="7"></sl-textarea>
|
||||
<sl-checkbox name="h" value="8"></sl-checkbox>
|
||||
</form>
|
||||
<input type="text" name="i" value="9" form="f1" />
|
||||
<sl-input type="text" name="j" value="10" form="f1"></sl-input>
|
||||
</div>
|
||||
`);
|
||||
const form = el.querySelector<HTMLFormElement>('form')!;
|
||||
|
||||
const formControls = getFormControls(form); // eslint-disable-line
|
||||
expect(formControls.length).to.equal(10); // eslint-disable-line
|
||||
expect(formControls.map((fc: HTMLInputElement) => fc.value).join('')).to.equal('12345678910'); // eslint-disable-line
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import '../icon/icon';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { defaultValue } from '../../internal/default-value';
|
||||
import { FormControlController } from '../../internal/form';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { html } from 'lit';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { live } from 'lit/directives/live.js';
|
||||
import { defaultValue } from '../../internal/default-value';
|
||||
import { FormSubmitController } from '../../internal/form';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { HasSlotController } from '../../internal/slot';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import '../icon/icon';
|
||||
import { watch } from '../../internal/watch';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import styles from './input.styles';
|
||||
import type { ShoelaceFormControl } from '../../internal/shoelace-element';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
import type { ShoelaceFormControl } from '../../internal/shoelace-element';
|
||||
|
||||
//
|
||||
// It's currently impossible to hide Firefox's built-in clear icon when using <input type="date|time">, so we need this
|
||||
// check to apply a clip-path to hide it. I know, I know...user agent sniffing is nasty but, if it fails, we only see a
|
||||
// check to apply a clip-path to hide it. I know, I know…user agent sniffing is nasty but, if it fails, we only see a
|
||||
// redundant clear icon so nothing important is breaking. The benefits outweigh the costs for this one. See the
|
||||
// discussion at: https://github.com/shoelace-style/shoelace/pull/794
|
||||
//
|
||||
@@ -27,51 +28,54 @@ const isFirefox = isChromium ? false : navigator.userAgent.includes('Firefox');
|
||||
|
||||
/**
|
||||
* @summary Inputs collect data from the user.
|
||||
*
|
||||
* @since 2.0
|
||||
* @documentation https://shoelace.style/components/input
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @dependency sl-icon
|
||||
*
|
||||
* @slot label - The input's label. Alternatively, you can use the `label` attribute.
|
||||
* @slot prefix - Used to prepend an icon or similar element to the input.
|
||||
* @slot suffix - Used to append an icon or similar element to the input.
|
||||
* @slot prefix - Used to prepend a presentational icon or similar element to the input.
|
||||
* @slot suffix - Used to append a presentational icon or similar element to the input.
|
||||
* @slot clear-icon - An icon to use in lieu of the default clear icon.
|
||||
* @slot show-password-icon - An icon to use in lieu of the default show password icon.
|
||||
* @slot hide-password-icon - An icon to use in lieu of the default hide password icon.
|
||||
* @slot help-text - Help text that describes how to use the input. Alternatively, you can use the `help-text` attribute.
|
||||
* @slot help-text - Text that describes how to use the input. Alternatively, you can use the `help-text` attribute.
|
||||
*
|
||||
* @event sl-blur - Emitted when the control loses focus.
|
||||
* @event sl-change - Emitted when an alteration to the control's value is committed by the user.
|
||||
* @event sl-clear - Emitted when the clear button is activated.
|
||||
* @event sl-input - Emitted when the control receives input and its value changes.
|
||||
* @event sl-focus - Emitted when the control gains focus.
|
||||
* @event sl-blur - Emitted when the control loses focus.
|
||||
* @event sl-input - Emitted when the control receives input.
|
||||
*
|
||||
* @csspart form-control - The form control that wraps the label, input, and help-text.
|
||||
* @csspart form-control - The form control that wraps the label, input, and help text.
|
||||
* @csspart form-control-label - The label's wrapper.
|
||||
* @csspart form-control-input - The input's wrapper.
|
||||
* @csspart form-control-help-text - The help text's wrapper.
|
||||
* @csspart base - The component's internal wrapper.
|
||||
* @csspart input - The input control.
|
||||
* @csspart prefix - The input prefix container.
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart input - The internal `<input>` control.
|
||||
* @csspart prefix - The container that wraps the prefix.
|
||||
* @csspart clear-button - The clear button.
|
||||
* @csspart password-toggle-button - The password toggle button.
|
||||
* @csspart suffix - The input suffix container.
|
||||
* @csspart suffix - The container that wraps the suffix.
|
||||
*/
|
||||
@customElement('sl-input')
|
||||
export default class SlInput extends ShoelaceElement implements ShoelaceFormControl {
|
||||
static styles: CSSResultGroup = styles;
|
||||
|
||||
@query('.input__control') input: HTMLInputElement;
|
||||
|
||||
private readonly formSubmitController = new FormSubmitController(this);
|
||||
private readonly formControlController = new FormControlController(this);
|
||||
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'label');
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
@state() private hasFocus = false;
|
||||
@state() invalid = false;
|
||||
@query('.input__control') input: HTMLInputElement;
|
||||
|
||||
/** The input's type. */
|
||||
@state() private hasFocus = false;
|
||||
@property() title = ''; // make reactive to pass through
|
||||
|
||||
/**
|
||||
* The type of input. Works the same as a native `<input>` element, but only a subset of types are supported. Defaults
|
||||
* to `text`.
|
||||
*/
|
||||
@property({ reflect: true }) type:
|
||||
| 'date'
|
||||
| 'datetime-local'
|
||||
@@ -84,50 +88,63 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
||||
| 'time'
|
||||
| 'url' = 'text';
|
||||
|
||||
/** The input's size. */
|
||||
@property({ reflect: true }) size: 'small' | 'medium' | 'large' = 'medium';
|
||||
|
||||
/** The input's name attribute. */
|
||||
/** The name of the input, submitted as a name/value pair with form data. */
|
||||
@property() name = '';
|
||||
|
||||
/** The input's value attribute. */
|
||||
/** The current value of the input, submitted as a name/value pair with form data. */
|
||||
@property() value = '';
|
||||
|
||||
/** Gets or sets the default value used to reset this element. The initial value corresponds to the one originally specified in the HTML that created this element. */
|
||||
/** 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, you can use the `label` slot instead. */
|
||||
/** 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, you can use the `help-text` slot instead. */
|
||||
/** The input's help text. If you need to display HTML, use the `help-text` slot instead. */
|
||||
@property({ attribute: 'help-text' }) helpText = '';
|
||||
|
||||
/** Adds a clear button when the input is populated. */
|
||||
/** Adds a clear button when the input is not empty. */
|
||||
@property({ type: Boolean }) clearable = false;
|
||||
|
||||
/** Adds a password toggle button to password inputs. */
|
||||
/** Disables the input. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
/** Placeholder text to show as a hint when the input is empty. */
|
||||
@property() placeholder = '';
|
||||
|
||||
/** Makes the input readonly. */
|
||||
@property({ type: Boolean, reflect: true }) readonly = false;
|
||||
|
||||
/** Adds a button to toggle the password's visibility. Only applies to password types. */
|
||||
@property({ attribute: 'password-toggle', type: Boolean }) passwordToggle = false;
|
||||
|
||||
/** Determines whether or not the password is currently visible. Only applies to password inputs. */
|
||||
/** Determines whether or not the password is currently visible. Only applies to password input types. */
|
||||
@property({ attribute: 'password-visible', type: Boolean }) passwordVisible = false;
|
||||
|
||||
/** Hides the browser's built-in increment/decrement spin buttons for number inputs. */
|
||||
@property({ attribute: 'no-spin-buttons', type: Boolean }) noSpinButtons = false;
|
||||
|
||||
/** The input's placeholder text. */
|
||||
@property() placeholder = '';
|
||||
/**
|
||||
* By default, form controls are associated with the nearest containing `<form>` element. This attribute allows you
|
||||
* to place the form control outside of a form and associate it with the form that has this `id`. The form must be in
|
||||
* the same document or shadow root for this to work.
|
||||
*/
|
||||
@property({ reflect: true }) form = '';
|
||||
|
||||
/** Disables the input. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
/** Makes the input a required field. */
|
||||
@property({ type: Boolean, reflect: true }) required = false;
|
||||
|
||||
/** Makes the input readonly. */
|
||||
@property({ type: Boolean, reflect: true }) readonly = false;
|
||||
/** A regular expression pattern to validate input against. */
|
||||
@property() pattern: string;
|
||||
|
||||
/** The minimum length of input that will be considered valid. */
|
||||
@property({ type: Number }) minlength: number;
|
||||
@@ -135,49 +152,54 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
||||
/** The maximum length of input that will be considered valid. */
|
||||
@property({ type: Number }) maxlength: number;
|
||||
|
||||
/** The input's minimum value. */
|
||||
@property() min: number;
|
||||
/** The input's minimum value. Only applies to date and number input types. */
|
||||
@property({ type: Number }) min: number;
|
||||
|
||||
/** The input's maximum value. */
|
||||
@property() max: number;
|
||||
/** The input's maximum value. Only applies to date and number input types. */
|
||||
@property({ type: Number }) max: number;
|
||||
|
||||
/**
|
||||
* Specifies the granularity that the value must adhere to, or the special value `any` which means no stepping is
|
||||
* implied, allowing any numeric value.
|
||||
* implied, allowing any numeric value. Only applies to date and number input types.
|
||||
*/
|
||||
@property() step: number | 'any';
|
||||
|
||||
/** A pattern to validate input against. */
|
||||
@property() pattern: string;
|
||||
|
||||
/** Makes the input a required field. */
|
||||
@property({ type: Boolean, reflect: true }) required = false;
|
||||
|
||||
/** The input's autocapitalize attribute. */
|
||||
/** Controls whether and how text input is automatically capitalized as it is entered by the user. */
|
||||
@property() autocapitalize: 'off' | 'none' | 'on' | 'sentences' | 'words' | 'characters';
|
||||
|
||||
/** The input's autocorrect attribute. */
|
||||
@property() autocorrect: string;
|
||||
|
||||
/** The input's autocomplete attribute. */
|
||||
@property() autocomplete: string;
|
||||
|
||||
/** The input's autofocus attribute. */
|
||||
@property({ type: Boolean }) autofocus: boolean;
|
||||
/** Indicates whether the browser's autocorrect feature is on or off. */
|
||||
@property() autocorrect: 'off' | 'on';
|
||||
|
||||
/**
|
||||
* The input's enterkeyhint attribute. This can be used to customize the label or icon of the Enter key on virtual
|
||||
* keyboards.
|
||||
* Specifies what permission the browser has to provide assistance in filling out form field values. Refer to
|
||||
* [this page on MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete) for available values.
|
||||
*/
|
||||
@property() autocomplete: string;
|
||||
|
||||
/** Indicates that the input should receive focus on page load. */
|
||||
@property({ type: Boolean }) autofocus: boolean;
|
||||
|
||||
/** Used to customize the label or icon of the Enter key on virtual keyboards. */
|
||||
@property() enterkeyhint: 'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send';
|
||||
|
||||
/** Enables spell checking on the input. */
|
||||
@property({ type: Boolean }) spellcheck: boolean;
|
||||
@property({
|
||||
type: Boolean,
|
||||
converter: {
|
||||
// Allow "true|false" attribute values but keep the property boolean
|
||||
fromAttribute: value => (!value || value === 'false' ? false : true),
|
||||
toAttribute: value => (value ? 'true' : 'false')
|
||||
}
|
||||
})
|
||||
spellcheck = true;
|
||||
|
||||
/** The input's inputmode attribute. */
|
||||
/**
|
||||
* Tells the browser what type of data will be entered by the user, allowing it to display the appropriate virtual
|
||||
* keyboard on supportive devices.
|
||||
*/
|
||||
@property() inputmode: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';
|
||||
|
||||
/** Gets or sets the current value as a `Date` object. Only valid when `type` is `date`. */
|
||||
/** Gets or sets the current value as a `Date` object. Returns `null` if the value can't be converted. */
|
||||
get valueAsDate() {
|
||||
return this.input?.valueAsDate ?? null;
|
||||
}
|
||||
@@ -190,7 +212,7 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
||||
this.value = input.value;
|
||||
}
|
||||
|
||||
/** Gets or sets the current value as a number. */
|
||||
/** Gets or sets the current value as a number. Returns `NaN` if the value can't be converted. */
|
||||
get valueAsNumber() {
|
||||
return this.input?.valueAsNumber ?? parseFloat(this.value);
|
||||
}
|
||||
@@ -204,7 +226,86 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.invalid = !this.input.checkValidity();
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
|
||||
private handleBlur() {
|
||||
this.hasFocus = false;
|
||||
this.emit('sl-blur');
|
||||
}
|
||||
|
||||
private handleChange() {
|
||||
this.value = this.input.value;
|
||||
this.emit('sl-change');
|
||||
}
|
||||
|
||||
private handleClearClick(event: MouseEvent) {
|
||||
this.value = '';
|
||||
this.emit('sl-clear');
|
||||
this.emit('sl-input');
|
||||
this.emit('sl-change');
|
||||
this.input.focus();
|
||||
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
private handleFocus() {
|
||||
this.hasFocus = true;
|
||||
this.emit('sl-focus');
|
||||
}
|
||||
|
||||
private handleInput() {
|
||||
this.value = this.input.value;
|
||||
this.formControlController.updateValidity();
|
||||
this.emit('sl-input');
|
||||
}
|
||||
|
||||
private handleInvalid() {
|
||||
this.formControlController.setValidity(false);
|
||||
}
|
||||
|
||||
private handleKeyDown(event: KeyboardEvent) {
|
||||
const hasModifier = event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
|
||||
|
||||
// Pressing enter when focused on an input should submit the form like a native input, but we wait a tick before
|
||||
// submitting to allow users to cancel the keydown event if they need to
|
||||
if (event.key === 'Enter' && !hasModifier) {
|
||||
setTimeout(() => {
|
||||
//
|
||||
// When using an Input Method Editor (IME), pressing enter will cause the form to submit unexpectedly. One way
|
||||
// to check for this is to look at event.isComposing, which will be true when the IME is open.
|
||||
//
|
||||
// See https://github.com/shoelace-style/shoelace/pull/988
|
||||
//
|
||||
if (!event.defaultPrevented && !event.isComposing) {
|
||||
this.formControlController.submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private handlePasswordToggle() {
|
||||
this.passwordVisible = !this.passwordVisible;
|
||||
}
|
||||
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
// Disabled form controls are always valid
|
||||
this.formControlController.setValidity(this.disabled);
|
||||
}
|
||||
|
||||
@watch('step', { waitUntilFirstUpdate: true })
|
||||
handleStepChange() {
|
||||
// If step changes, the value may become invalid so we need to recheck after the update. We set the new step
|
||||
// imperatively so we don't have to wait for the next render to report the updated validity.
|
||||
this.input.step = String(this.step);
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
|
||||
@watch('value', { waitUntilFirstUpdate: true })
|
||||
async handleValueChange() {
|
||||
await this.updateComplete;
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
|
||||
/** Sets focus on the input. */
|
||||
@@ -234,16 +335,15 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
||||
/** Replaces a range of text with a new string. */
|
||||
setRangeText(
|
||||
replacement: string,
|
||||
start: number,
|
||||
end: number,
|
||||
selectMode: 'select' | 'start' | 'end' | 'preserve' = 'preserve'
|
||||
start?: number,
|
||||
end?: number,
|
||||
selectMode?: 'select' | 'start' | 'end' | 'preserve'
|
||||
) {
|
||||
// @ts-expect-error - start, end, and selectMode are optional
|
||||
this.input.setRangeText(replacement, start, end, selectMode);
|
||||
|
||||
if (this.value !== this.input.value) {
|
||||
this.value = this.input.value;
|
||||
this.emit('sl-input');
|
||||
this.emit('sl-change');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,8 +359,6 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
||||
this.input.stepUp();
|
||||
if (this.value !== this.input.value) {
|
||||
this.value = this.input.value;
|
||||
this.emit('sl-input');
|
||||
this.emit('sl-change');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,8 +367,6 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
||||
this.input.stepDown();
|
||||
if (this.value !== this.input.value) {
|
||||
this.value = this.input.value;
|
||||
this.emit('sl-input');
|
||||
this.emit('sl-change');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,89 +380,10 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
||||
return this.input.reportValidity();
|
||||
}
|
||||
|
||||
/** Sets a custom validation message. If `message` is not empty, the field will be considered invalid. */
|
||||
/** Sets a custom validation message. Pass an empty string to restore validity. */
|
||||
setCustomValidity(message: string) {
|
||||
this.input.setCustomValidity(message);
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
|
||||
handleBlur() {
|
||||
this.hasFocus = false;
|
||||
this.emit('sl-blur');
|
||||
}
|
||||
|
||||
handleChange() {
|
||||
this.value = this.input.value;
|
||||
this.emit('sl-change');
|
||||
}
|
||||
|
||||
handleClearClick(event: MouseEvent) {
|
||||
this.value = '';
|
||||
this.emit('sl-clear');
|
||||
this.emit('sl-input');
|
||||
this.emit('sl-change');
|
||||
this.input.focus();
|
||||
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
@watch('disabled', { waitUntilFirstUpdate: true })
|
||||
handleDisabledChange() {
|
||||
// Disabled form controls are always valid, so we need to recheck validity when the state changes
|
||||
this.input.disabled = this.disabled;
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
|
||||
@watch('step', { waitUntilFirstUpdate: true })
|
||||
handleStepChange() {
|
||||
// If step changes, the value may become invalid so we need to recheck after the update. We set the new step
|
||||
// imperatively so we don't have to wait for the next render to report the updated validity.
|
||||
this.input.step = String(this.step);
|
||||
this.invalid = !this.input.checkValidity();
|
||||
}
|
||||
|
||||
handleFocus() {
|
||||
this.hasFocus = true;
|
||||
this.emit('sl-focus');
|
||||
}
|
||||
|
||||
handleInput() {
|
||||
this.value = this.input.value;
|
||||
this.emit('sl-input');
|
||||
}
|
||||
|
||||
handleInvalid() {
|
||||
this.invalid = true;
|
||||
}
|
||||
|
||||
handleKeyDown(event: KeyboardEvent) {
|
||||
const hasModifier = event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
|
||||
|
||||
// Pressing enter when focused on an input should submit the form like a native input, but we wait a tick before
|
||||
// submitting to allow users to cancel the keydown event if they need to
|
||||
if (event.key === 'Enter' && !hasModifier) {
|
||||
setTimeout(() => {
|
||||
//
|
||||
// When using an Input Method Editor (IME), pressing enter will cause the form to submit unexpectedly. One way
|
||||
// to check for this is to look at event.isComposing, which will be true when the IME is open.
|
||||
//
|
||||
// See https://github.com/shoelace-style/shoelace/pull/988
|
||||
//
|
||||
if (!event.defaultPrevented && !event.isComposing) {
|
||||
this.formSubmitController.submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handlePasswordToggle() {
|
||||
this.passwordVisible = !this.passwordVisible;
|
||||
}
|
||||
|
||||
@watch('value', { waitUntilFirstUpdate: true })
|
||||
handleValueChange() {
|
||||
this.input.value = this.value; // force a sync update
|
||||
this.invalid = !this.input.checkValidity();
|
||||
this.formControlController.updateValidity();
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -416,20 +433,17 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
||||
'input--disabled': this.disabled,
|
||||
'input--focused': this.hasFocus,
|
||||
'input--empty': !this.value,
|
||||
'input--invalid': this.invalid,
|
||||
'input--no-spin-buttons': this.noSpinButtons,
|
||||
'input--is-firefox': isFirefox
|
||||
})}
|
||||
>
|
||||
<span part="prefix" class="input__prefix">
|
||||
<slot name="prefix"></slot>
|
||||
</span>
|
||||
|
||||
<slot name="prefix" part="prefix" class="input__prefix"></slot>
|
||||
<input
|
||||
part="input"
|
||||
id="input"
|
||||
class="input__control"
|
||||
type=${this.type === 'password' && this.passwordVisible ? 'text' : this.type}
|
||||
title=${this.title /* An empty title prevents browser validation tooltips from appearing on hover */}
|
||||
name=${ifDefined(this.name)}
|
||||
?disabled=${this.disabled}
|
||||
?readonly=${this.readonly}
|
||||
@@ -445,12 +459,11 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
||||
autocomplete=${ifDefined(this.type === 'password' ? 'off' : this.autocomplete)}
|
||||
autocorrect=${ifDefined(this.type === 'password' ? 'off' : this.autocorrect)}
|
||||
?autofocus=${this.autofocus}
|
||||
spellcheck=${ifDefined(this.spellcheck)}
|
||||
spellcheck=${this.spellcheck}
|
||||
pattern=${ifDefined(this.pattern)}
|
||||
enterkeyhint=${ifDefined(this.enterkeyhint)}
|
||||
inputmode=${ifDefined(this.inputmode)}
|
||||
aria-describedby="help-text"
|
||||
aria-invalid=${this.invalid ? 'true' : 'false'}
|
||||
@change=${this.handleChange}
|
||||
@input=${this.handleInput}
|
||||
@invalid=${this.handleInvalid}
|
||||
@@ -459,60 +472,64 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
|
||||
@blur=${this.handleBlur}
|
||||
/>
|
||||
|
||||
${hasClearIcon
|
||||
? html`
|
||||
<button
|
||||
part="clear-button"
|
||||
class="input__clear"
|
||||
type="button"
|
||||
aria-label=${this.localize.term('clearEntry')}
|
||||
@click=${this.handleClearClick}
|
||||
tabindex="-1"
|
||||
>
|
||||
<slot name="clear-icon">
|
||||
<sl-icon name="x-circle-fill" library="system"></sl-icon>
|
||||
</slot>
|
||||
</button>
|
||||
`
|
||||
: ''}
|
||||
${this.passwordToggle && !this.disabled
|
||||
? html`
|
||||
<button
|
||||
part="password-toggle-button"
|
||||
class="input__password-toggle"
|
||||
type="button"
|
||||
aria-label=${this.localize.term(this.passwordVisible ? 'hidePassword' : 'showPassword')}
|
||||
@click=${this.handlePasswordToggle}
|
||||
tabindex="-1"
|
||||
>
|
||||
${this.passwordVisible
|
||||
? html`
|
||||
<slot name="show-password-icon">
|
||||
<sl-icon name="eye-slash" library="system"></sl-icon>
|
||||
</slot>
|
||||
`
|
||||
: html`
|
||||
<slot name="hide-password-icon">
|
||||
<sl-icon name="eye" library="system"></sl-icon>
|
||||
</slot>
|
||||
`}
|
||||
</button>
|
||||
`
|
||||
: ''}
|
||||
${
|
||||
hasClearIcon
|
||||
? html`
|
||||
<button
|
||||
part="clear-button"
|
||||
class="input__clear"
|
||||
type="button"
|
||||
aria-label=${this.localize.term('clearEntry')}
|
||||
@click=${this.handleClearClick}
|
||||
tabindex="-1"
|
||||
>
|
||||
<slot name="clear-icon">
|
||||
<sl-icon name="x-circle-fill" library="system"></sl-icon>
|
||||
</slot>
|
||||
</button>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
${
|
||||
this.passwordToggle && !this.disabled
|
||||
? html`
|
||||
<button
|
||||
part="password-toggle-button"
|
||||
class="input__password-toggle"
|
||||
type="button"
|
||||
aria-label=${this.localize.term(this.passwordVisible ? 'hidePassword' : 'showPassword')}
|
||||
@click=${this.handlePasswordToggle}
|
||||
tabindex="-1"
|
||||
>
|
||||
${this.passwordVisible
|
||||
? html`
|
||||
<slot name="show-password-icon">
|
||||
<sl-icon name="eye-slash" library="system"></sl-icon>
|
||||
</slot>
|
||||
`
|
||||
: html`
|
||||
<slot name="hide-password-icon">
|
||||
<sl-icon name="eye" library="system"></sl-icon>
|
||||
</slot>
|
||||
`}
|
||||
</button>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
|
||||
<span part="suffix" class="input__suffix">
|
||||
<slot name="suffix"></slot>
|
||||
</span>
|
||||
<slot name="suffix" part="suffix" class="input__suffix"></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
<slot
|
||||
name="help-text"
|
||||
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>
|
||||
${this.helpText}
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -8,6 +8,10 @@ export default css`
|
||||
display: block;
|
||||
}
|
||||
|
||||
:host([inert]) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -33,6 +37,7 @@ export default css`
|
||||
|
||||
.menu-item .menu-item__label {
|
||||
flex: 1 1 auto;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.menu-item .menu-item__prefix {
|
||||
@@ -41,7 +46,7 @@ export default css`
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.menu-item .menu-item__prefix ::slotted(*) {
|
||||
.menu-item .menu-item__prefix::slotted(*) {
|
||||
margin-inline-end: var(--sl-spacing-x-small);
|
||||
}
|
||||
|
||||
@@ -51,7 +56,7 @@ export default css`
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.menu-item .menu-item__suffix ::slotted(*) {
|
||||
.menu-item .menu-item__suffix::slotted(*) {
|
||||
margin-inline-start: var(--sl-spacing-x-small);
|
||||
}
|
||||
|
||||
@@ -59,11 +64,16 @@ export default css`
|
||||
outline: none;
|
||||
}
|
||||
|
||||
:host(:hover:not([aria-disabled='true'])) .menu-item,
|
||||
:host(:focus-visible:not(.sl-focus-invisible):not([aria-disabled='true'])) .menu-item {
|
||||
:host(:hover:not([aria-disabled='true'])) .menu-item {
|
||||
background-color: var(--sl-color-neutral-100);
|
||||
color: var(--sl-color-neutral-1000);
|
||||
}
|
||||
|
||||
:host(:focus-visible) .menu-item {
|
||||
outline: none;
|
||||
background-color: var(--sl-color-primary-600);
|
||||
color: var(--sl-color-neutral-0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.menu-item .menu-item__check,
|
||||
@@ -83,7 +93,7 @@ export default css`
|
||||
|
||||
@media (forced-colors: active) {
|
||||
:host(:hover:not([aria-disabled='true'])) .menu-item,
|
||||
:host(:focus-visible:not(.sl-focus-invisible):not([aria-disabled='true'])) .menu-item {
|
||||
:host(:focus-visible) .menu-item {
|
||||
outline: dashed 1px SelectedItem;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
@@ -1,50 +1,64 @@
|
||||
import { expect, fixture, html, waitUntil, aTimeout } from '@open-wc/testing';
|
||||
import { aTimeout, expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
import sinon from 'sinon';
|
||||
import type SlMenuItem from './menu-item';
|
||||
|
||||
describe('<sl-menu-item>', () => {
|
||||
it('passes accessibility test', async () => {
|
||||
it('should pass accessibility tests', async () => {
|
||||
const el = await fixture<SlMenuItem>(html`
|
||||
<sl-select>
|
||||
<sl-menu-item>Test</sl-menu-item>
|
||||
</sl-select>
|
||||
<sl-menu>
|
||||
<sl-menu-item>Item 1</sl-menu-item>
|
||||
<sl-menu-item>Item 2</sl-menu-item>
|
||||
<sl-menu-item>Item 3</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item type="checkbox" checked>Checked</sl-menu-item>
|
||||
<sl-menu-item type="checkbox">Unchecked</sl-menu-item>
|
||||
</sl-menu>
|
||||
`);
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('default properties', async () => {
|
||||
it('should have the correct default properties', async () => {
|
||||
const el = await fixture<SlMenuItem>(html` <sl-menu-item>Test</sl-menu-item> `);
|
||||
|
||||
expect(el.checked).to.be.false;
|
||||
expect(el.getAttribute('aria-checked')).to.equal('false');
|
||||
expect(el.value).to.equal('');
|
||||
expect(el.disabled).to.be.false;
|
||||
expect(el.getAttribute('aria-disabled')).to.equal('false');
|
||||
});
|
||||
|
||||
it('changes aria attributes', async () => {
|
||||
it('should render the correct aria attributes when disabled', async () => {
|
||||
const el = await fixture<SlMenuItem>(html` <sl-menu-item>Test</sl-menu-item> `);
|
||||
|
||||
el.checked = true;
|
||||
await aTimeout(100);
|
||||
expect(el.getAttribute('aria-checked')).to.equal('true');
|
||||
el.disabled = true;
|
||||
await aTimeout(100);
|
||||
expect(el.getAttribute('aria-disabled')).to.equal('true');
|
||||
});
|
||||
|
||||
it('get text label', async () => {
|
||||
it('should return a text label when calling getTextLabel()', async () => {
|
||||
const el = await fixture<SlMenuItem>(html` <sl-menu-item>Test</sl-menu-item> `);
|
||||
expect(el.getTextLabel()).to.equal('Test');
|
||||
});
|
||||
|
||||
it('emit sl-label-change event on label change', async () => {
|
||||
const el = await fixture<SlMenuItem>(html` <sl-menu-item>Test</sl-menu-item> `);
|
||||
it('should emit the slotchange event when the label changes', async () => {
|
||||
const el = await fixture<SlMenuItem>(html` <sl-menu-item>Text</sl-menu-item> `);
|
||||
const slotChangeHandler = sinon.spy();
|
||||
|
||||
const labelChangeHandler = sinon.spy();
|
||||
el.addEventListener('slotchange', slotChangeHandler);
|
||||
el.textContent = 'New Text';
|
||||
el.addEventListener('sl-label-change', labelChangeHandler);
|
||||
await waitUntil(() => labelChangeHandler.calledOnce);
|
||||
expect(labelChangeHandler).to.have.been.calledOnce;
|
||||
await waitUntil(() => slotChangeHandler.calledOnce);
|
||||
|
||||
expect(slotChangeHandler).to.have.been.calledOnce;
|
||||
});
|
||||
|
||||
it('should render a hidden menu item when the inert attribute is used', async () => {
|
||||
const menu = await fixture<SlMenuItem>(html`
|
||||
<sl-menu>
|
||||
<sl-menu-item inert>Item 1</sl-menu-item>
|
||||
<sl-menu-item>Item 2</sl-menu-item>
|
||||
<sl-menu-item>Item 3</sl-menu-item>
|
||||
</sl-menu>
|
||||
`);
|
||||
const item1 = menu.querySelector('sl-menu-item')!;
|
||||
|
||||
expect(getComputedStyle(item1).display).to.equal('none');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,29 +1,26 @@
|
||||
import { html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import { getTextContent } from '../../internal/slot';
|
||||
import { watch } from '../../internal/watch';
|
||||
import '../icon/icon';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { getTextContent } from '../../internal/slot';
|
||||
import { html } from 'lit';
|
||||
import { watch } from '../../internal/watch';
|
||||
import ShoelaceElement from '../../internal/shoelace-element';
|
||||
import styles from './menu-item.styles';
|
||||
import type { CSSResultGroup } from 'lit';
|
||||
|
||||
/**
|
||||
* @summary Menu items provide options for the user to pick from in a menu.
|
||||
*
|
||||
* @since 2.0
|
||||
* @documentation https://shoelace.style/components/menu-item
|
||||
* @status stable
|
||||
* @since 2.0
|
||||
*
|
||||
* @dependency sl-icon
|
||||
*
|
||||
* @event sl-label-change - Emitted when the menu item's text label changes. For performance reasons, this event is only
|
||||
* emitted if the default slot's `slotchange` event is triggered. It will not fire when the label is first set.
|
||||
*
|
||||
* @slot - The menu item's label.
|
||||
* @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.
|
||||
*
|
||||
* @csspart base - The component's internal 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 prefix - The prefix container.
|
||||
* @csspart label - The menu item label.
|
||||
@@ -38,35 +35,19 @@ export default class SlMenuItem extends ShoelaceElement {
|
||||
@query('slot:not([name])') defaultSlot: HTMLSlotElement;
|
||||
@query('.menu-item') menuItem: HTMLElement;
|
||||
|
||||
/** The type of menu item to render. To use `checked`, this value must be set to `checkbox`. */
|
||||
@property() type: 'normal' | 'checkbox' = 'normal';
|
||||
|
||||
/** Draws the item in a checked state. */
|
||||
@property({ type: Boolean, reflect: true }) checked = false;
|
||||
|
||||
/** A unique value to store in the menu item. This can be used as a way to identify menu items when selected. */
|
||||
@property() value = '';
|
||||
|
||||
/** Draws the menu item in a disabled state. */
|
||||
/** Draws the menu item in a disabled state, preventing selection. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
firstUpdated() {
|
||||
this.setAttribute('role', 'menuitem');
|
||||
}
|
||||
|
||||
/** Returns a text label based on the contents of the menu item's default slot. */
|
||||
getTextLabel() {
|
||||
return getTextContent(this.defaultSlot);
|
||||
}
|
||||
|
||||
@watch('checked')
|
||||
handleCheckedChange() {
|
||||
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
|
||||
}
|
||||
|
||||
@watch('disabled')
|
||||
handleDisabledChange() {
|
||||
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
|
||||
}
|
||||
|
||||
handleDefaultSlotChange() {
|
||||
private handleDefaultSlotChange() {
|
||||
const textLabel = this.getTextLabel();
|
||||
|
||||
// Ignore the first time the label is set
|
||||
@@ -75,12 +56,51 @@ export default class SlMenuItem extends ShoelaceElement {
|
||||
return;
|
||||
}
|
||||
|
||||
// When the label changes, emit a slotchange event so parent controls see it
|
||||
if (textLabel !== this.cachedTextLabel) {
|
||||
this.cachedTextLabel = textLabel;
|
||||
this.emit('sl-label-change');
|
||||
this.emit('slotchange', { bubbles: true, composed: false, cancelable: false });
|
||||
}
|
||||
}
|
||||
|
||||
@watch('checked')
|
||||
handleCheckedChange() {
|
||||
// For proper accessibility, users have to use type="checkbox" to use the checked attribute
|
||||
if (this.checked && this.type !== 'checkbox') {
|
||||
this.checked = false;
|
||||
console.error('The checked attribute can only be used on menu items with type="checkbox"', this);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only checkbox types can receive the aria-checked attribute
|
||||
if (this.type === 'checkbox') {
|
||||
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
|
||||
} else {
|
||||
this.removeAttribute('aria-checked');
|
||||
}
|
||||
}
|
||||
|
||||
@watch('disabled')
|
||||
handleDisabledChange() {
|
||||
this.setAttribute('aria-disabled', this.disabled ? 'true' : 'false');
|
||||
}
|
||||
|
||||
@watch('type')
|
||||
handleTypeChange() {
|
||||
if (this.type === 'checkbox') {
|
||||
this.setAttribute('role', 'menuitemcheckbox');
|
||||
this.setAttribute('aria-checked', this.checked ? 'true' : 'false');
|
||||
} else {
|
||||
this.setAttribute('role', 'menuitem');
|
||||
this.removeAttribute('aria-checked');
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns a text label based on the contents of the menu item's default slot. */
|
||||
getTextLabel() {
|
||||
return getTextContent(this.defaultSlot);
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div
|
||||
@@ -96,17 +116,11 @@ export default class SlMenuItem extends ShoelaceElement {
|
||||
<sl-icon name="check" library="system" aria-hidden="true"></sl-icon>
|
||||
</span>
|
||||
|
||||
<span part="prefix" class="menu-item__prefix">
|
||||
<slot name="prefix"></slot>
|
||||
</span>
|
||||
<slot name="prefix" part="prefix" class="menu-item__prefix"></slot>
|
||||
|
||||
<span part="label" class="menu-item__label">
|
||||
<slot @slotchange=${this.handleDefaultSlotChange}></slot>
|
||||
</span>
|
||||
<slot part="label" class="menu-item__label" @slotchange=${this.handleDefaultSlotChange}></slot>
|
||||
|
||||
<span part="suffix" class="menu-item__suffix">
|
||||
<slot name="suffix"></slot>
|
||||
</span>
|
||||
<slot name="suffix" part="suffix" class="menu-item__suffix"></slot>
|
||||
|
||||
<span class="menu-item__chevron">
|
||||
<sl-icon name="chevron-right" library="system" aria-hidden="true"></sl-icon>
|
||||
|
||||
@@ -9,6 +9,7 @@ export default css`
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
display: inline-block;
|
||||
font-family: var(--sl-font-sans);
|
||||
font-size: var(--sl-font-size-small);
|
||||
font-weight: var(--sl-font-weight-semibold);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user