mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-19 07:29:14 +00:00
Compare commits
133 Commits
v2.0.0-bet
...
context-me
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ee519d40a | ||
|
|
6bc17d48c3 | ||
|
|
a1263f1b9d | ||
|
|
d69ebab765 | ||
|
|
0504946dac | ||
|
|
fbd6691711 | ||
|
|
aec17da6b0 | ||
|
|
639533662d | ||
|
|
a340ce4a68 | ||
|
|
6e5fe64e8b | ||
|
|
84bdbb84b8 | ||
|
|
f91ffb6cb4 | ||
|
|
13815199a3 | ||
|
|
98c20ff551 | ||
|
|
479b6b9081 | ||
|
|
c640d2ea77 | ||
|
|
715547d2fd | ||
|
|
8a914a536b | ||
|
|
f56b6c0648 | ||
|
|
25aa8318d9 | ||
|
|
72f2cbe9e8 | ||
|
|
fc7836084a | ||
|
|
60d9d9202b | ||
|
|
a9df468282 | ||
|
|
0bba773c3e | ||
|
|
7be03ae623 | ||
|
|
d4741532f5 | ||
|
|
10f31efefa | ||
|
|
be662ddf32 | ||
|
|
ff84beaade | ||
|
|
8dba8fa5fb | ||
|
|
3a3f5552a7 | ||
|
|
88cba353c0 | ||
|
|
a2851370bb | ||
|
|
7c0ef7dcf0 | ||
|
|
fb6d5d89b8 | ||
|
|
45ceff4c08 | ||
|
|
6169abc700 | ||
|
|
c09e12d13e | ||
|
|
6152e15e10 | ||
|
|
79910b2ae8 | ||
|
|
c347df7c17 | ||
|
|
e9e2b35c59 | ||
|
|
8ae753c396 | ||
|
|
d2c94321f2 | ||
|
|
4c10f8a537 | ||
|
|
9a19cc2173 | ||
|
|
c4cbc894f5 | ||
|
|
449f5e6c7f | ||
|
|
34447a3f2f | ||
|
|
eee97d7dba | ||
|
|
f16392947a | ||
|
|
c3adf92b49 | ||
|
|
a6580b018d | ||
|
|
00c843c7ce | ||
|
|
5fe55a4db9 | ||
|
|
4ca998c346 | ||
|
|
42b3e2cc11 | ||
|
|
59fb8db6be | ||
|
|
664beafefa | ||
|
|
04443a64e2 | ||
|
|
92dedf3386 | ||
|
|
2ba5fb9820 | ||
|
|
222235159b | ||
|
|
1061bd5e0d | ||
|
|
ccec9a8348 | ||
|
|
bf9e06e67d | ||
|
|
1e03d222c5 | ||
|
|
3722f46b8e | ||
|
|
8d7bf97127 | ||
|
|
d26c1a6407 | ||
|
|
f296ff8476 | ||
|
|
a75a71994a | ||
|
|
7d1373a1d1 | ||
|
|
23abc50015 | ||
|
|
5e28d1131a | ||
|
|
a141e64a69 | ||
|
|
e12ee97bd9 | ||
|
|
ebd84642e1 | ||
|
|
888ac2ea0d | ||
|
|
37068c922c | ||
|
|
eec24d2ed1 | ||
|
|
8fa9d629a3 | ||
|
|
ef22dd7dc4 | ||
|
|
3f12624b78 | ||
|
|
d6fa67374c | ||
|
|
d4183cf718 | ||
|
|
4ad0480039 | ||
|
|
7188425ac0 | ||
|
|
c83581cf47 | ||
|
|
f8fa29f157 | ||
|
|
f2a4db6291 | ||
|
|
a4aff0b1e9 | ||
|
|
f5c2e0b425 | ||
|
|
2f88c55ec0 | ||
|
|
db7075a91a | ||
|
|
8ac007ba9a | ||
|
|
111fa8397c | ||
|
|
0ef4e92a96 | ||
|
|
9123afac18 | ||
|
|
1b5dca52e3 | ||
|
|
bc3e9c43da | ||
|
|
ded01cfd8a | ||
|
|
c737b7494f | ||
|
|
47aff56e71 | ||
|
|
15ce341d81 | ||
|
|
7476204258 | ||
|
|
e083b1a02e | ||
|
|
fa74fc54e3 | ||
|
|
fbbeec6d2f | ||
|
|
b4192364f6 | ||
|
|
c7dc82947f | ||
|
|
c38fd3986c | ||
|
|
35d7926e18 | ||
|
|
86e06ce1e6 | ||
|
|
1a08f063a6 | ||
|
|
740208ed76 | ||
|
|
521f6fe3f1 | ||
|
|
f43d490763 | ||
|
|
898179b645 | ||
|
|
8c3888da02 | ||
|
|
e1471ec9a1 | ||
|
|
eac07a51ba | ||
|
|
753000b56a | ||
|
|
8ab2907c3f | ||
|
|
723ee80c8f | ||
|
|
e1c003c8df | ||
|
|
96c82dc69f | ||
|
|
0dd9483797 | ||
|
|
4da110087d | ||
|
|
7f75a64647 | ||
|
|
17df0e3cd3 | ||
|
|
76df2fd204 |
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,38 +1,36 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Create a report to help us improve.
|
||||
about: Create a bug report to help us fix a demonstrable problem with code in the library.
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: claviska
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
### Describe the bug
|
||||
A bug is _a demonstrable problem_ caused by code in the library. Please provide a clear and concise description of what the bug is here.
|
||||
|
||||
**To Reproduce**
|
||||
### To Reproduce
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
2. Click on '...'
|
||||
3. Scroll down to '...'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
### Demo
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
If the bug isn't obvious, please provide a link to a CodePen or Fiddle with a minimal reproduction. Bugs that have repros get attention faster than those that don't.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
Tip: use the CodePen button on any example in the docs!
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
### Screenshots
|
||||
If applicable, add screenshots to help explain the bug.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
### Browser / OS
|
||||
- OS: [e.g. Mac, Windows]
|
||||
- Browser [e.g. Chrome, Firefox, Safari]
|
||||
- Browser version [e.g. 22]
|
||||
|
||||
### Additional information
|
||||
Provide any additional information about the bug here.
|
||||
|
||||
15
.github/ISSUE_TEMPLATE/feature_request.md
vendored
15
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -7,14 +7,11 @@ assignees: claviska
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
### What issue are you having?
|
||||
Provide a clear and concise description of the problem you're facing.
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
### Describe the solution you'd like
|
||||
How would you like to see the library solve it?
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
### Describe alternatives you've considered
|
||||
In what ways have you tried to solve this with the current version?
|
||||
|
||||
31
.github/workflows/node.js.yml
vendored
Normal file
31
.github/workflows/node.js.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||
|
||||
name: Node.js CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ next ]
|
||||
pull_request:
|
||||
branches: [ next ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x, 16.x]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
- run: npm ci
|
||||
- run: npm run build --if-present
|
||||
- run: npm test
|
||||
@@ -23,7 +23,7 @@ Twitter: [@shoelace_style](https://twitter.com/shoelace_style)
|
||||
|
||||
## Shoemakers 🥾
|
||||
|
||||
Shoemakers, or "Shoelace developers," can use this documentation to learn how to build Shoelace from source. You will need Node >= 12.10.0 to build and run the project locally.
|
||||
Shoemakers, or "Shoelace developers," can use this documentation to learn how to build Shoelace from source. You will need Node >= 14 to build and run the project locally. It is preferred, but not required, to use npm 7.
|
||||
|
||||
**You don't need to do any of this to use Shoelace!** This page is for people who want to contribute to the project, tinker with the source, or create a custom build of Shoelace.
|
||||
|
||||
|
||||
@@ -21,8 +21,10 @@
|
||||
- [Card](/components/card)
|
||||
- [Checkbox](/components/checkbox)
|
||||
- [Color Picker](/components/color-picker)
|
||||
- [Context Menu](/components/context-menu)
|
||||
- [Details](/components/details)
|
||||
- [Dialog](/components/dialog)
|
||||
- [Divider](/components/divider)
|
||||
- [Drawer](/components/drawer)
|
||||
- [Dropdown](/components/dropdown)
|
||||
- [Form](/components/form)
|
||||
@@ -31,7 +33,6 @@
|
||||
- [Image Comparer](/components/image-comparer)
|
||||
- [Input](/components/input)
|
||||
- [Menu](/components/menu)
|
||||
- [Menu Divider](/components/menu-divider)
|
||||
- [Menu Item](/components/menu-item)
|
||||
- [Menu Label](/components/menu-label)
|
||||
- [Progress Bar](/components/progress-bar)
|
||||
@@ -54,11 +55,13 @@
|
||||
<!--plop:component-->
|
||||
|
||||
- Utilities
|
||||
- [Animated Image](/components/animated-image)
|
||||
- [Animation](/components/animation)
|
||||
- [Format Bytes](/components/format-bytes)
|
||||
- [Format Date](/components/format-date)
|
||||
- [Format Number](/components/format-number)
|
||||
- [Include](/components/include)
|
||||
- [Mutation Observer](/components/mutation-observer)
|
||||
- [Relative Time](/components/relative-time)
|
||||
- [Resize Observer](/components/resize-observer)
|
||||
- [Responsive Media](/components/responsive-media)
|
||||
@@ -73,5 +76,6 @@
|
||||
- [Z-index](/tokens/z-index)
|
||||
|
||||
- Tutorials
|
||||
- [Integrating with Laravel](/tutorials/integrating-with-laravel)
|
||||
- [Integrating with NextJS](/tutorials/integrating-with-nextjs)
|
||||
- [Integrating with Rails](/tutorials/integrating-with-rails)
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 709 KiB After Width: | Height: | Size: 688 KiB |
BIN
docs/assets/images/tie.webp
Normal file
BIN
docs/assets/images/tie.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
BIN
docs/assets/images/walk.gif
Normal file
BIN
docs/assets/images/walk.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 MiB |
@@ -11,7 +11,6 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Attribute</th>
|
||||
<th>Description</th>
|
||||
<th>Reflects</th>
|
||||
<th>Type</th>
|
||||
@@ -21,18 +20,40 @@
|
||||
<tbody>
|
||||
${props
|
||||
.map(prop => {
|
||||
const hasAttribute = !!prop.attribute;
|
||||
const isAttributeDifferent = prop.attribute !== prop.name;
|
||||
let attributeInfo = '';
|
||||
|
||||
if (!hasAttribute) {
|
||||
attributeInfo = `<br><small>(property only)</small>`;
|
||||
} else if (isAttributeDifferent) {
|
||||
attributeInfo = `
|
||||
<br>
|
||||
<sl-tooltip content="This attribute is different than the property">
|
||||
<small>
|
||||
<code class="nowrap">
|
||||
${escapeHtml(prop.attribute)}
|
||||
</code>
|
||||
</small>
|
||||
</sl-tooltip>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td class="nowrap"><code>${escapeHtml(prop.name)}</code></td>
|
||||
<td class="nowrap">${prop.attribute ? `<code>${escapeHtml(prop.attribute)}</code>` : '-'}</td>
|
||||
<td>${escapeHtml(prop.description)}</td>
|
||||
<td>
|
||||
<code class="nowrap">${escapeHtml(prop.name)}</code>
|
||||
${attributeInfo}
|
||||
</td>
|
||||
<td>
|
||||
${escapeHtml(prop.description)}
|
||||
</td>
|
||||
<td style="text-align: center;">${
|
||||
prop.reflects ? '<sl-icon label="yes" name="check"></sl-icon>' : ''
|
||||
}</td>
|
||||
<td>${prop.type?.text ? `<code>${escapeHtml(prop.type?.text || '')}</code>` : '-'}</td>
|
||||
<td>${prop.default ? `<code>${escapeHtml(prop.default)}</code>` : '-'}</td>
|
||||
</tr>
|
||||
`;
|
||||
`;
|
||||
})
|
||||
.join('')}
|
||||
</tbody>
|
||||
@@ -55,12 +76,12 @@
|
||||
${events
|
||||
.map(
|
||||
event => `
|
||||
<tr>
|
||||
<td><code class="nowrap">${escapeHtml(event.name)}</code></td>
|
||||
<td>${escapeHtml(event.description)}</td>
|
||||
<td>${event.type?.text ? `<code>${escapeHtml(event.type?.text)}` : '-'}</td>
|
||||
</tr>
|
||||
`
|
||||
<tr>
|
||||
<td><code class="nowrap">${escapeHtml(event.name)}</code></td>
|
||||
<td>${escapeHtml(event.description)}</td>
|
||||
<td>${event.type?.text ? `<code>${escapeHtml(event.type?.text)}` : '-'}</td>
|
||||
</tr>
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
</tbody>
|
||||
@@ -146,11 +167,11 @@
|
||||
${styles
|
||||
.map(
|
||||
style => `
|
||||
<tr>
|
||||
<td><code>${escapeHtml(style.name)}</code></td>
|
||||
<td>${escapeHtml(style.description)}</td>
|
||||
</tr>
|
||||
`
|
||||
<tr>
|
||||
<td><code>${escapeHtml(style.name)}</code></td>
|
||||
<td>${escapeHtml(style.description)}</td>
|
||||
</tr>
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
</tbody>
|
||||
@@ -172,11 +193,11 @@
|
||||
${parts
|
||||
.map(
|
||||
part => `
|
||||
<tr>
|
||||
<td class="nowrap"><code>${escapeHtml(part.name)}</code></td>
|
||||
<td>${escapeHtml(part.description)}</td>
|
||||
</tr>
|
||||
`
|
||||
<tr>
|
||||
<td class="nowrap"><code>${escapeHtml(part.name)}</code></td>
|
||||
<td>${escapeHtml(part.description)}</td>
|
||||
</tr>
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
</tbody>
|
||||
@@ -198,11 +219,11 @@
|
||||
${animations
|
||||
.map(
|
||||
animation => `
|
||||
<tr>
|
||||
<td class="nowrap"><code>${escapeHtml(animation.name)}</code></td>
|
||||
<td>${escapeHtml(animation.description)}</td>
|
||||
</tr>
|
||||
`
|
||||
<tr>
|
||||
<td class="nowrap"><code>${escapeHtml(animation.name)}</code></td>
|
||||
<td>${escapeHtml(animation.description)}</td>
|
||||
</tr>
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
</tbody>
|
||||
|
||||
@@ -39,7 +39,7 @@ body.site-search-visible {
|
||||
flex-direction: column;
|
||||
max-width: 460px;
|
||||
max-height: calc(100vh - 20rem);
|
||||
background-color: rgb(var(--sl-color-neutral-0));
|
||||
background-color: rgb(var(--sl-surface-base-alt));
|
||||
border-radius: var(--sl-border-radius-large);
|
||||
box-shadow: var(--sl-shadow-x-large);
|
||||
margin: 10rem auto;
|
||||
@@ -55,6 +55,7 @@ body.site-search-visible {
|
||||
|
||||
.site-search__input::part(base) {
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: var(--sl-border-radius-large);
|
||||
}
|
||||
|
||||
|
||||
@@ -221,7 +221,12 @@
|
||||
await searchIndex;
|
||||
|
||||
const hasQuery = query.length > 0;
|
||||
const matches = hasQuery ? searchIndex.search(`${query}~2`) : [];
|
||||
let matches = hasQuery ? searchIndex.search(`${query}`) : [];
|
||||
|
||||
// Fall back to a fuzzy search if no matches are found
|
||||
if (matches.length === 0 && hasQuery) {
|
||||
matches = searchIndex.search(`${query}~2`);
|
||||
}
|
||||
|
||||
let hasResults = hasQuery && matches.length > 0;
|
||||
siteSearch.classList.toggle('site-search--has-results', hasQuery && hasResults);
|
||||
|
||||
@@ -9,9 +9,21 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.theme-picker sl-menu-label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.theme-picker sl-menu-label kbd {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.theme-picker {
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
}
|
||||
|
||||
.theme-picker sl-menu-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,9 +47,10 @@
|
||||
<sl-icon name="sun" label="Select Theme"></sl-icon>
|
||||
</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-divider></sl-menu-divider>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item value="auto">Auto</sl-menu-item>
|
||||
</sl-menu>
|
||||
`;
|
||||
@@ -63,6 +64,19 @@
|
||||
// Update the theme when the preference changes
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addListener(event => setTheme(theme));
|
||||
|
||||
// Toggle themes when pressing backslash
|
||||
document.addEventListener('keydown', event => {
|
||||
if (
|
||||
event.key === '\\' &&
|
||||
!event.composedPath().some(el => ['input', 'textarea'].includes(el?.tagName?.toLowerCase()))
|
||||
) {
|
||||
event.preventDefault();
|
||||
|
||||
setTheme(isDark() ? 'light' : 'dark');
|
||||
show();
|
||||
}
|
||||
});
|
||||
|
||||
// Set the intial theme and sync the UI
|
||||
setTheme(theme);
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ body {
|
||||
font-size: var(--sl-font-size-medium);
|
||||
font-weight: var(--sl-font-weight-normal);
|
||||
letter-spacing: var(--sl-letter-spacing-normal);
|
||||
background-color: rgb(var(--sl-color-neutral-0));
|
||||
background-color: rgb(var(--sl-surface-base));
|
||||
color: rgb(var(--sl-color-neutral-800));
|
||||
line-height: var(--sl-line-height-normal);
|
||||
}
|
||||
@@ -32,7 +32,7 @@ strong {
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
background: rgb(var(--sl-color-neutral-0));
|
||||
background-color: rgb(var(--sl-surface-base));
|
||||
border-right: solid 1px rgb(var(--sl-color-neutral-200));
|
||||
}
|
||||
|
||||
@@ -54,94 +54,6 @@ strong {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Search */
|
||||
.sidebar .search {
|
||||
position: relative;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.sidebar .search input[type='search'] {
|
||||
background-color: rgb(var(--sl-input-background-color));
|
||||
border: solid 1px rgb(var(--sl-input-border-color));
|
||||
border-radius: var(--sl-border-radius-pill);
|
||||
color: rgb(var(--sl-input-color));
|
||||
padding-left: 1rem;
|
||||
padding-right: 2rem;
|
||||
margin: 0 1.25rem;
|
||||
transition: var(--sl-transition-fast) color, var(--sl-transition-fast) border, var(--sl-transition-fast) box-shadow;
|
||||
}
|
||||
|
||||
.sidebar .search input[type='search']::placeholder {
|
||||
color: rgb(var(--sl-input-placeholder-color));
|
||||
}
|
||||
|
||||
.sidebar .search input[type='search']:hover {
|
||||
background-color: rgb(var(--sl-input-background-color-hover));
|
||||
border-color: rgb(var(--sl-input-border-color-hover));
|
||||
color: rgb(var(--sl-input-color-hover));
|
||||
}
|
||||
|
||||
.sidebar .search input[type='search']:focus {
|
||||
background-color: rgb(var(--sl-input-background-color-focus));
|
||||
box-shadow: var(--sl-focus-ring);
|
||||
border-color: rgb(var(--sl-input-border-color-focus));
|
||||
color: rgb(var(--sl-input-color-focus));
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.sidebar .input-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
|
||||
.sidebar .clear-button {
|
||||
position: absolute;
|
||||
right: 34px;
|
||||
top: 7px;
|
||||
width: 22px !important;
|
||||
height: 22px !important;
|
||||
}
|
||||
|
||||
.sidebar .clear-button svg {
|
||||
transform: scale(0.75) !important;
|
||||
}
|
||||
|
||||
.sidebar .clear-button svg circle {
|
||||
fill: rgb(var(--sl-color-neutral-500));
|
||||
}
|
||||
|
||||
.sidebar .clear-button svg path {
|
||||
stroke: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
|
||||
.sidebar .clear-button:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search .results-panel {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.search .matching-post {
|
||||
border-bottom: solid 1px rgb(var(--sl-color-neutral-600)) !important;
|
||||
padding: 0.25rem 1.5rem;
|
||||
}
|
||||
|
||||
.search .matching-post a {
|
||||
display: block;
|
||||
border-radius: inherit;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.search .matching-post h2 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.search .matching-post p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* Sidebar toggle */
|
||||
.sidebar-toggle {
|
||||
top: 0.25rem;
|
||||
@@ -149,7 +61,7 @@ strong {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: var(--sl-border-radius-medium);
|
||||
background-color: rgb(var(--sl-color-neutral-0));
|
||||
background-color: rgb(var(--sl-surface-base));
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -317,10 +229,6 @@ strong {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.markdown-section sl-tab-panel::part(base) {
|
||||
padding: var(--sl-spacing-medium) 0;
|
||||
}
|
||||
|
||||
.docsify-pagination-container {
|
||||
border-top-color: rgb(var(--sl-color-neutral-200)) !important;
|
||||
}
|
||||
@@ -383,11 +291,15 @@ strong {
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.markdown-section tr:nth-child(2n) code {
|
||||
background-color: rgb(var(--sl-color-neutral-100));
|
||||
}
|
||||
|
||||
kbd,
|
||||
.markdown-section kbd {
|
||||
font-family: var(--sl-font-mono);
|
||||
font-size: 87.5%;
|
||||
background-color: rgb(var(--sl-color-neutral-100));
|
||||
background-color: rgb(var(--sl-color-neutral-50));
|
||||
border-radius: var(--sl-border-radius-small);
|
||||
border: solid 1px rgb(var(--sl-color-neutral-200));
|
||||
box-shadow: inset 0 1px 0 rgb(var(--sl-color-neutral-0));
|
||||
@@ -521,6 +433,11 @@ kbd,
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.markdown-section table sl-tooltip code {
|
||||
border-bottom: dashed 1px rgb(var(--sl-color-neutral-300));
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
/* Iframes */
|
||||
.markdown-section iframe {
|
||||
border: none;
|
||||
@@ -673,6 +590,7 @@ body[data-page^='/tokens/'] .table-wrapper td:first-child code {
|
||||
border-radius: 3px;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
/* Color palettes */
|
||||
|
||||
63
docs/components/animated-image.md
Normal file
63
docs/components/animated-image.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Animated Image
|
||||
|
||||
[component-header:sl-animated-image]
|
||||
|
||||
A component for displaying animated GIFs and WEBPs that play and pause on interaction.
|
||||
|
||||
```html preview
|
||||
<sl-animated-image
|
||||
src="/assets/images/walk.gif"
|
||||
alt="Animation of untied shoes walking on pavement"
|
||||
></sl-animated-image>
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### WEBP Images
|
||||
|
||||
Both GIF and WEBP images are supported.
|
||||
|
||||
```html preview
|
||||
<sl-animated-image
|
||||
src="/assets/images/tie.webp"
|
||||
alt="Animation of a shoe being tied"
|
||||
></sl-animated-image>
|
||||
```
|
||||
|
||||
### Setting a Width and Height
|
||||
|
||||
To set a custom size, apply a width and/or height to the host element.
|
||||
|
||||
```html preview
|
||||
<sl-animated-image
|
||||
src="/assets/images/walk.gif"
|
||||
alt="Animation of untied shoes walking on pavement"
|
||||
style="width: 150px; height: 200px;"
|
||||
>
|
||||
</sl-animated-image>
|
||||
```
|
||||
|
||||
### Customizing the Control Box
|
||||
|
||||
You can change the appearance and location of the control box by targeting the `control-box` part in your styles.
|
||||
|
||||
```html preview
|
||||
<sl-animated-image
|
||||
src="/assets/images/walk.gif"
|
||||
alt="Animation of untied shoes walking on pavement"
|
||||
class="animated-image-custom-control-box"
|
||||
></sl-animated-image>
|
||||
|
||||
<style>
|
||||
.animated-image-custom-control-box::part(control-box) {
|
||||
top: auto;
|
||||
right: auto;
|
||||
bottom: 1rem;
|
||||
left: 1rem;
|
||||
background-color: deeppink;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
[component-metadata:sl-animated-image]
|
||||
@@ -49,7 +49,7 @@ Use the `separator` slot to change the separator that goes between breadcrumb it
|
||||
|
||||
```html preview
|
||||
<sl-breadcrumb>
|
||||
<sl-icon name="dot" slot="separator" id="dotty"></sl-icon>
|
||||
<sl-icon name="dot" slot="separator"></sl-icon>
|
||||
<sl-breadcrumb-item>First</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>Second</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>Third</sl-breadcrumb-item>
|
||||
|
||||
@@ -33,6 +33,19 @@ Use the `size` attribute to change a button's size.
|
||||
<sl-button size="large">Large</sl-button>
|
||||
```
|
||||
|
||||
### Outline Buttons
|
||||
|
||||
Use the `outline` attribute to draw outlined buttons with transparent backgrounds.
|
||||
|
||||
```html preview
|
||||
<sl-button type="default" outline>Default</sl-button>
|
||||
<sl-button type="primary" outline>Primary</sl-button>
|
||||
<sl-button type="success" outline>Success</sl-button>
|
||||
<sl-button type="neutral" outline>Neutral</sl-button>
|
||||
<sl-button type="warning" outline>Warning</sl-button>
|
||||
<sl-button type="danger" outline>Danger</sl-button>
|
||||
```
|
||||
|
||||
### Pill Buttons
|
||||
|
||||
Use the `pill` attribute to give buttons rounded edges.
|
||||
|
||||
140
docs/components/context-menu.md
Normal file
140
docs/components/context-menu.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# Context Menu
|
||||
|
||||
[component-header:sl-context-menu]
|
||||
|
||||
Context menus offer additional options through a menu that opens at the pointer's location, usually activated by a right-click.
|
||||
|
||||
Context menus are designed to work with [menus](/components/menu) and [menu items](/components/menu-item). The menu must include `slot="menu"`. Other content you provide will be part of the context menu's target area.
|
||||
|
||||
```html preview
|
||||
<sl-context-menu>
|
||||
<div style="height: 200px; background: rgb(var(--sl-color-neutral-100)); display: flex; align-items: center; justify-content: center; padding: 1rem;">
|
||||
Right-click to activate the context menu
|
||||
</div>
|
||||
|
||||
<sl-menu slot="menu">
|
||||
<sl-menu-item value="undo">Undo</sl-menu-item>
|
||||
<sl-menu-item value="redo">Redo</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
<sl-menu-item value="delete">Delete</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-context-menu>
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Handling Selections
|
||||
|
||||
The [menu component](/components/menu) emits an `sl-select` event when a menu item is selected. You can use this to handle selections. The selected item will be available in `event.detail.item`.
|
||||
|
||||
```html preview
|
||||
<div class="context-menu-selections">
|
||||
<sl-context-menu>
|
||||
<div style="height: 200px; background: rgb(var(--sl-color-neutral-100)); display: flex; align-items: center; justify-content: center; padding: 1rem;">
|
||||
Right-click to activate the context menu
|
||||
</div>
|
||||
|
||||
<sl-menu slot="menu">
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-context-menu>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const container = document.querySelector('.context-menu-selections');
|
||||
const menu = container.querySelector('sl-menu');
|
||||
const result = container.querySelector('.result');
|
||||
|
||||
menu.addEventListener('sl-select', event => {
|
||||
console.log(`You selected: ${event.detail.item.value}`);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### Inline
|
||||
|
||||
The context menu uses `display: contents`, so it will assume the shape of the content you slot in.
|
||||
|
||||
```html preview
|
||||
<sl-context-menu>
|
||||
<span style="background: rgb(var(--sl-color-neutral-100)); padding: .5rem 1rem;">
|
||||
Right-click here
|
||||
</span>
|
||||
|
||||
<sl-menu slot="menu">
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-context-menu>
|
||||
```
|
||||
|
||||
### Placement
|
||||
|
||||
The preferred placement of the context menu can be set with the `placement` attribute. Note that the actual position may vary to ensure the menu remains in the viewport.
|
||||
|
||||
```html preview
|
||||
<sl-context-menu placement="top-end">
|
||||
<div style="height: 200px; background: rgb(var(--sl-color-neutral-100)); display: flex; align-items: center; justify-content: center; padding: 1rem;">
|
||||
Right-click to activate the context menu
|
||||
</div>
|
||||
|
||||
<sl-menu slot="menu">
|
||||
<sl-menu-item value="undo">Undo</sl-menu-item>
|
||||
<sl-menu-item value="redo">Redo</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
<sl-menu-item value="delete">Delete</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-context-menu>
|
||||
```
|
||||
|
||||
### Detecting the Target Item
|
||||
|
||||
A single context menu can wrap a number of items. To detect the item that activated the context menu...
|
||||
|
||||
TODO
|
||||
|
||||
```html preview
|
||||
<div class="context-menu-detecting">
|
||||
<sl-context-menu>
|
||||
<ul>
|
||||
<li>Item 1</li>
|
||||
<li>Item 2</li>
|
||||
<li>Item 3</li>
|
||||
<li>Item 4</li>
|
||||
<li>Item 5</li>
|
||||
</ul>
|
||||
|
||||
<sl-menu slot="menu">
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-context-menu>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.context-menu-detecting ul {
|
||||
max-width: 300px;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.context-menu-detecting li {
|
||||
background: rgb(var(--sl-color-neutral-100));
|
||||
padding: .5rem 1rem;
|
||||
margin: 0 0 2px 0;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
[component-metadata:sl-context-menu]
|
||||
71
docs/components/divider.md
Normal file
71
docs/components/divider.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Divider
|
||||
|
||||
[component-header:sl-divider]
|
||||
|
||||
Dividers are used to visually separate or group elements.
|
||||
|
||||
```html preview
|
||||
<sl-divider></sl-divider>
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Width
|
||||
|
||||
Use the `--width` custom property to change the width of the divider.
|
||||
|
||||
```html preview
|
||||
<sl-divider style="--width: 4px;"></sl-divider>
|
||||
```
|
||||
|
||||
### Color
|
||||
|
||||
Use the `--color` custom property to change the color of the divider.
|
||||
|
||||
```html preview
|
||||
<sl-divider style="--color: tomato;"></sl-divider>
|
||||
```
|
||||
|
||||
### Spacing
|
||||
|
||||
Use the `--spacing` custom property to change the amount of space between the divider and it's neighboring elements.
|
||||
|
||||
```html preview
|
||||
<div style="text-align: center;">
|
||||
Above
|
||||
<sl-divider style="--spacing: 2rem;"></sl-divider>
|
||||
Below
|
||||
</div>
|
||||
```
|
||||
|
||||
### Vertical
|
||||
|
||||
Add the `vertical` attribute to draw the divider in a vertical orientation. The divider will span the full height of its container. Vertical dividers work especially well inside of a flex container.
|
||||
|
||||
```html preview
|
||||
<div style="display: flex; align-items: center; height: 2rem;">
|
||||
First
|
||||
<sl-divider vertical></sl-divider>
|
||||
Middle
|
||||
<sl-divider vertical></sl-divider>
|
||||
Last
|
||||
</div>
|
||||
```
|
||||
|
||||
### Menu Dividers
|
||||
|
||||
Use dividers in [menus](/components/menu) to visually group menu items.
|
||||
|
||||
```html preview
|
||||
<sl-menu style="max-width: 200px; border: solid 1px rgb(var(--sl-panel-border-color)); border-radius: var(--sl-border-radius-medium);">
|
||||
<sl-menu-item value="1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="2">Option 2</sl-menu-item>
|
||||
<sl-menu-item value="3">Option 3</sl-menu-item>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item value="4">Option 4</sl-menu-item>
|
||||
<sl-menu-item value="5">Option 5</sl-menu-item>
|
||||
<sl-menu-item value="6">Option 6</sl-menu-item>
|
||||
</sl-menu>
|
||||
```
|
||||
|
||||
[component-metadata:sl-divider]
|
||||
@@ -15,10 +15,10 @@ Dropdowns are designed to work well with [menus](/components/menu) to provide a
|
||||
<sl-menu-item>Dropdown Item 1</sl-menu-item>
|
||||
<sl-menu-item>Dropdown Item 2</sl-menu-item>
|
||||
<sl-menu-item>Dropdown Item 3</sl-menu-item>
|
||||
<sl-menu-divider></sl-menu-divider>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item checked>Checked</sl-menu-item>
|
||||
<sl-menu-item disabled>Disabled</sl-menu-item>
|
||||
<sl-menu-divider></sl-menu-divider>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item>
|
||||
Prefix
|
||||
<sl-icon slot="prefix" name="gift"></sl-icon>
|
||||
@@ -33,6 +33,33 @@ Dropdowns are designed to work well with [menus](/components/menu) to provide a
|
||||
|
||||
## Examples
|
||||
|
||||
### Getting the Selected Item
|
||||
|
||||
When dropdowns are used with [menus](/components/menu), you can listen for the `sl-select` event to determine which menu item was selected. The menu item element will be exposed in `event.detail.item`. You can set `value` props to make it easier to identify commands.
|
||||
|
||||
```html preview
|
||||
<div class="dropdown-selection">
|
||||
<sl-dropdown>
|
||||
<sl-button slot="trigger" caret>Edit</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const container = document.querySelector('.dropdown-selection');
|
||||
const dropdown = container.querySelector('sl-dropdown');
|
||||
|
||||
dropdown.addEventListener('sl-select', event => {
|
||||
const selectedItem = event.detail.item;
|
||||
console.log(selectedItem.value);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### Placement
|
||||
|
||||
The preferred placement of the dropdown can be set with the `placement` attribute. Note that the actual position may vary to ensure the panel remains in the viewport.
|
||||
@@ -44,7 +71,7 @@ The preferred placement of the dropdown can be set with the `placement` attribut
|
||||
<sl-menu-item>Cut</sl-menu-item>
|
||||
<sl-menu-item>Copy</sl-menu-item>
|
||||
<sl-menu-item>Paste</sl-menu-item>
|
||||
<sl-menu-divider></sl-menu-divider>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item>Find</sl-menu-item>
|
||||
<sl-menu-item>Replace</sl-menu-item>
|
||||
</sl-menu>
|
||||
@@ -62,7 +89,7 @@ The distance from the panel to the trigger can be customized using the `distance
|
||||
<sl-menu-item>Cut</sl-menu-item>
|
||||
<sl-menu-item>Copy</sl-menu-item>
|
||||
<sl-menu-item>Paste</sl-menu-item>
|
||||
<sl-menu-divider></sl-menu-divider>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item>Find</sl-menu-item>
|
||||
<sl-menu-item>Replace</sl-menu-item>
|
||||
</sl-menu>
|
||||
@@ -80,7 +107,7 @@ The offset of the panel along the trigger can be customized using the `skidding`
|
||||
<sl-menu-item>Cut</sl-menu-item>
|
||||
<sl-menu-item>Copy</sl-menu-item>
|
||||
<sl-menu-item>Paste</sl-menu-item>
|
||||
<sl-menu-divider></sl-menu-divider>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item>Find</sl-menu-item>
|
||||
<sl-menu-item>Replace</sl-menu-item>
|
||||
</sl-menu>
|
||||
@@ -121,57 +148,4 @@ Dropdown panels will be clipped if they're inside a container that has `overflow
|
||||
</style>
|
||||
```
|
||||
|
||||
### Getting the Selected Item
|
||||
|
||||
When dropdowns are used with [menus](/components/menu), you can listen for the `sl-select` event to determine which menu item was selected. The menu item element will be exposed in `event.detail.item`. You can set `value` props to make it easier to identify commands.
|
||||
|
||||
```html preview
|
||||
<div class="dropdown-selection">
|
||||
<sl-dropdown>
|
||||
<sl-button slot="trigger" caret>Edit</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const container = document.querySelector('.dropdown-selection');
|
||||
const dropdown = container.querySelector('sl-dropdown');
|
||||
|
||||
dropdown.addEventListener('sl-select', event => {
|
||||
const selectedItem = event.detail.item;
|
||||
console.log(selectedItem.value);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
Alternatively, you can listen for the `click` event on individual menu items. Note that, using this approach, disabled menu items will still emit a `click` event.
|
||||
|
||||
```html preview
|
||||
<div class="dropdown-selection-alt">
|
||||
<sl-dropdown>
|
||||
<sl-button slot="trigger" caret>Edit</sl-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const container = document.querySelector('.dropdown-selection-alt');
|
||||
const cut = container.querySelector('sl-menu-item[value="cut"]');
|
||||
const copy = container.querySelector('sl-menu-item[value="copy"]');
|
||||
const paste = container.querySelector('sl-menu-item[value="paste"]');
|
||||
|
||||
cut.addEventListener('click', () => console.log('cut'));
|
||||
copy.addEventListener('click', () => console.log('copy'));
|
||||
paste.addEventListener('click', () => console.log('paste'));
|
||||
</script>
|
||||
```
|
||||
|
||||
[component-metadata:sl-dropdown]
|
||||
|
||||
@@ -42,6 +42,14 @@ Add the `toggle-password` attribute to add a toggle button that will show the pa
|
||||
<sl-input type="password" placeholder="Password Toggle" size="large" toggle-password></sl-input>
|
||||
```
|
||||
|
||||
### Filled Inputs
|
||||
|
||||
Add the `filled` attribute to draw a filled input.
|
||||
|
||||
```html preview
|
||||
<sl-input placeholder="Type something" filled></sl-input>
|
||||
```
|
||||
|
||||
### Pill
|
||||
|
||||
Use the `pill` attribute to give inputs rounded edges.
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
# Menu Divider
|
||||
|
||||
[component-header:sl-menu-divider]
|
||||
|
||||
Menu dividers are used to visually group menu items.
|
||||
|
||||
```html preview
|
||||
<sl-menu
|
||||
style="max-width: 200px; border: solid 1px rgb(var(--sl-panel-border-color)); border-radius: var(--sl-border-radius-medium);"
|
||||
>
|
||||
<sl-menu-item value="1">Option 1</sl-menu-item>
|
||||
<sl-menu-item value="2">Option 2</sl-menu-item>
|
||||
<sl-menu-divider></sl-menu-divider>
|
||||
<sl-menu-item value="3">Option 3</sl-menu-item>
|
||||
<sl-menu-item value="4">Option 4</sl-menu-item>
|
||||
<sl-menu-divider></sl-menu-divider>
|
||||
<sl-menu-item value="5">Option 5</sl-menu-item>
|
||||
<sl-menu-item value="6">Option 6</sl-menu-item>
|
||||
</sl-menu>
|
||||
```
|
||||
|
||||
[component-metadata:sl-menu-divider]
|
||||
@@ -5,16 +5,14 @@
|
||||
Menu items provide options for the user to pick from in a menu.
|
||||
|
||||
```html preview
|
||||
<sl-menu
|
||||
style="max-width: 200px; border: solid 1px rgb(var(--sl-panel-border-color)); border-radius: var(--sl-border-radius-medium);"
|
||||
>
|
||||
<sl-menu style="max-width: 200px; border: solid 1px rgb(var(--sl-panel-border-color)); border-radius: var(--sl-border-radius-medium);">
|
||||
<sl-menu-item>Option 1</sl-menu-item>
|
||||
<sl-menu-item>Option 2</sl-menu-item>
|
||||
<sl-menu-item>Option 3</sl-menu-item>
|
||||
<sl-menu-divider></sl-menu-divider>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item checked>Checked</sl-menu-item>
|
||||
<sl-menu-item disabled>Disabled</sl-menu-item>
|
||||
<sl-menu-divider></sl-menu-divider>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item>
|
||||
Prefix Icon
|
||||
<sl-icon slot="prefix" name="gift"></sl-icon>
|
||||
@@ -26,4 +24,82 @@ Menu items provide options for the user to pick from in a menu.
|
||||
</sl-menu>
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Checked
|
||||
|
||||
Use the `checked` attribute to draw menu items in a checked state.
|
||||
|
||||
```html preview
|
||||
<sl-menu style="max-width: 200px; border: solid 1px rgb(var(--sl-panel-border-color)); border-radius: var(--sl-border-radius-medium);">
|
||||
<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>
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
Add the `disabled` attribute to disable the menu item so it cannot be selected.
|
||||
|
||||
```html preview
|
||||
<sl-menu style="max-width: 200px; border: solid 1px rgb(var(--sl-panel-border-color)); border-radius: var(--sl-border-radius-medium);">
|
||||
<sl-menu-item>Option 1</sl-menu-item>
|
||||
<sl-menu-item disabled>Option 2</sl-menu-item>
|
||||
<sl-menu-item>Option 3</sl-menu-item>
|
||||
</sl-menu>
|
||||
```
|
||||
|
||||
### Prefix & Suffix
|
||||
|
||||
Add content to the start and end of menu items using the `prefix` and `suffix` slots.
|
||||
|
||||
```html preview
|
||||
<sl-menu style="max-width: 200px; border: solid 1px rgb(var(--sl-panel-border-color)); border-radius: var(--sl-border-radius-medium);">
|
||||
<sl-menu-item>
|
||||
<sl-icon slot="prefix" name="house"></sl-icon>
|
||||
Home
|
||||
</sl-menu-item>
|
||||
|
||||
<sl-menu-item>
|
||||
<sl-icon slot="prefix" name="envelope"></sl-icon>
|
||||
Messages
|
||||
<sl-badge slot="suffix" type="primary" pill>12</sl-badge>
|
||||
</sl-menu-item>
|
||||
|
||||
<sl-divider></sl-divider>
|
||||
|
||||
<sl-menu-item>
|
||||
<sl-icon slot="prefix" name="gear"></sl-icon>
|
||||
Settings
|
||||
</sl-menu-item>
|
||||
</sl-menu>
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
```html preview
|
||||
<sl-menu class="menu-value" style="max-width: 200px; border: solid 1px rgb(var(--sl-panel-border-color)); border-radius: var(--sl-border-radius-medium);">
|
||||
<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-menu>
|
||||
|
||||
<script>
|
||||
const menu = document.querySelector('.menu-value');
|
||||
|
||||
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}`);
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
[component-metadata:sl-menu-item]
|
||||
|
||||
@@ -12,7 +12,7 @@ Menu labels are used to describe a group of menu items.
|
||||
<sl-menu-item value="apple">Apple</sl-menu-item>
|
||||
<sl-menu-item value="banana">Banana</sl-menu-item>
|
||||
<sl-menu-item value="orange">Orange</sl-menu-item>
|
||||
<sl-menu-divider></sl-menu-divider>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-label>Vegetables</sl-menu-label>
|
||||
<sl-menu-item value="broccoli">Broccoli</sl-menu-item>
|
||||
<sl-menu-item value="carrot">Carrot</sl-menu-item>
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
|
||||
Menus provide a list of options for the user to choose from.
|
||||
|
||||
You can use [menu items](/components/menu-item), [menu dividers](/components/menu-divider), and [menu labels](/components/menu-label) to compose a menu. Menus support keyboard interactions, including type-to-select an option.
|
||||
You can use [menu items](/components/menu-item), [menu labels](/components/menu-label), and [dividers](/components/divider) to compose a menu. Menus support keyboard interactions, including type-to-select an option.
|
||||
|
||||
```html preview
|
||||
<sl-menu style="max-width: 200px; border: solid 1px rgb(var(--sl-panel-border-color)); border-radius: var(--sl-border-radius-medium);">
|
||||
<sl-menu-item value="undo">Undo</sl-menu-item>
|
||||
<sl-menu-item value="redo">Redo</sl-menu-item>
|
||||
<sl-menu-divider></sl-menu-divider>
|
||||
<sl-divider></sl-divider>
|
||||
<sl-menu-item value="cut">Cut</sl-menu-item>
|
||||
<sl-menu-item value="copy">Copy</sl-menu-item>
|
||||
<sl-menu-item value="paste">Paste</sl-menu-item>
|
||||
|
||||
104
docs/components/mutation-observer.md
Normal file
104
docs/components/mutation-observer.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# Mutation Observer
|
||||
|
||||
[component-header:sl-mutation-observer]
|
||||
|
||||
The Mutation Observer component offers a thin, declarative interface to the [`MutationObserver API`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver).
|
||||
|
||||
The mutation observer will report changes to the content it wraps through the `sl-mutation` event. When emitted, a collection of [MutationRecord](https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord) objects will be attached to `event.detail` that contains information about how it changed.
|
||||
|
||||
```html preview
|
||||
<div class="mutation-overview">
|
||||
<sl-mutation-observer attr>
|
||||
<sl-button type="primary">Click to mutate</sl-button>
|
||||
</sl-mutation-observer>
|
||||
|
||||
<br>
|
||||
👆 Click the button and watch the console
|
||||
|
||||
<script>
|
||||
const container = document.querySelector('.mutation-overview');
|
||||
const mutationObserver = container.querySelector('sl-mutation-observer');
|
||||
const button = container.querySelector('sl-button');
|
||||
const types = ['primary', 'success', 'neutral', 'warning', 'danger'];
|
||||
let clicks = 0;
|
||||
|
||||
// Change the button's type attribute
|
||||
button.addEventListener('click', () => {
|
||||
clicks++;
|
||||
button.setAttribute('type', types[clicks % types.length]);
|
||||
});
|
||||
|
||||
// Log mutations
|
||||
mutationObserver.addEventListener('sl-mutation', event => {
|
||||
console.log(event.detail);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.mutation-overview sl-button {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
```
|
||||
|
||||
?> When you create a mutation observer, you must indicate what changes it should respond to by including at least one of `attr`, `child-list`, or `char-data`. If you don't specify at least one of these attributes, no mutation events will be emitted.
|
||||
|
||||
## Examples
|
||||
|
||||
### Child List
|
||||
|
||||
Use the `child-list` attribute to watch for new child elements that are added or removed.
|
||||
|
||||
```html preview
|
||||
<div class="mutation-child-list">
|
||||
<sl-mutation-observer child-list>
|
||||
<div class="buttons">
|
||||
<sl-button type="primary">Add button</sl-button>
|
||||
</div>
|
||||
</sl-mutation-observer>
|
||||
|
||||
👆 Add and remove buttons and watch the console
|
||||
|
||||
<script>
|
||||
const container = document.querySelector('.mutation-child-list');
|
||||
const mutationObserver = container.querySelector('sl-mutation-observer');
|
||||
const buttons = container.querySelector('.buttons');
|
||||
const button = container.querySelector('sl-button[type="primary"]');
|
||||
let i = 0;
|
||||
|
||||
// Add a button
|
||||
button.addEventListener('click', () => {
|
||||
const button = document.createElement('sl-button');
|
||||
button.textContent = ++i;
|
||||
buttons.append(button);
|
||||
});
|
||||
|
||||
// Remove a button
|
||||
buttons.addEventListener('click', event => {
|
||||
const target = event.target.closest('sl-button:not([type="primary"])');
|
||||
event.stopPropagation();
|
||||
|
||||
if (target) {
|
||||
target.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Log mutations
|
||||
mutationObserver.addEventListener('sl-mutation', event => {
|
||||
console.log(event.detail);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.mutation-child-list .buttons {
|
||||
display: flex;
|
||||
gap: .25rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
```
|
||||
|
||||
[component-metadata:sl-mutation-observer]
|
||||
@@ -5,7 +5,7 @@
|
||||
Progress bars are used to show the status of an ongoing operation.
|
||||
|
||||
```html preview
|
||||
<sl-progress-bar percentage="50"></sl-progress-bar>
|
||||
<sl-progress-bar value="50"></sl-progress-bar>
|
||||
```
|
||||
|
||||
## Examples
|
||||
@@ -15,15 +15,23 @@ Progress bars are used to show the status of an ongoing operation.
|
||||
Use the `--height` custom property to set the progress bar's height.
|
||||
|
||||
```html preview
|
||||
<sl-progress-bar percentage="50" style="--height: 6px;"></sl-progress-bar>
|
||||
<sl-progress-bar value="50" style="--height: 6px;"></sl-progress-bar>
|
||||
```
|
||||
|
||||
### Labels
|
||||
|
||||
Use the default slot to show a label.
|
||||
Use the `label` attribute to label the progress bar and tell assistive devices how to announce it.
|
||||
|
||||
```html preview
|
||||
<sl-progress-bar percentage="50" class="progress-bar-labels">50%</sl-progress-bar>
|
||||
<sl-progress-bar value="50" label="Upload progress"></sl-progress-bar>
|
||||
```
|
||||
|
||||
### Showing Values
|
||||
|
||||
Use the default slot to show a value.
|
||||
|
||||
```html preview
|
||||
<sl-progress-bar value="50" class="progress-bar-values">50%</sl-progress-bar>
|
||||
|
||||
<br>
|
||||
|
||||
@@ -31,27 +39,27 @@ Use the default slot to show a label.
|
||||
<sl-button circle><sl-icon name="plus"></sl-icon></sl-button>
|
||||
|
||||
<script>
|
||||
const progressBar = document.querySelector('.progress-bar-labels');
|
||||
const progressBar = document.querySelector('.progress-bar-values');
|
||||
const subtractButton = progressBar.nextElementSibling.nextElementSibling;
|
||||
const addButton = subtractButton.nextElementSibling;
|
||||
|
||||
addButton.addEventListener('click', () => {
|
||||
const percentage = Math.min(100, progressBar.percentage + 10);
|
||||
progressBar.percentage = percentage;
|
||||
progressBar.textContent = `${percentage}%`;
|
||||
const value = Math.min(100, progressBar.value + 10);
|
||||
progressBar.value = value;
|
||||
progressBar.textContent = `${value}%`;
|
||||
});
|
||||
|
||||
subtractButton.addEventListener('click', () => {
|
||||
const percentage = Math.max(0, progressBar.percentage - 10)
|
||||
progressBar.percentage = percentage;
|
||||
progressBar.textContent = `${percentage}%`;
|
||||
const value = Math.max(0, progressBar.value - 10)
|
||||
progressBar.value = value;
|
||||
progressBar.textContent = `${value}%`;
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
### Indeterminate
|
||||
|
||||
The `indeterminate` attribute can be used to inform the user that the operation is pending, but its status cannot currently be determined. In this state, `percentage` is ignored and the label, if present, will not be shown.
|
||||
The `indeterminate` attribute can be used to inform the user that the operation is pending, but its status cannot currently be determined. In this state, `value` is ignored and the label, if present, will not be shown.
|
||||
|
||||
```html preview
|
||||
<sl-progress-bar indeterminate></sl-progress-bar>
|
||||
|
||||
@@ -5,25 +5,25 @@
|
||||
Progress rings are used to show the progress of a determinate operation in a circular fashion.
|
||||
|
||||
```html preview
|
||||
<sl-progress-ring percentage="25"></sl-progress-ring>
|
||||
<sl-progress-ring value="25"></sl-progress-ring>
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Size
|
||||
|
||||
Use the `size` attribute to set the diameter of the progress ring.
|
||||
Use the `--size` custom property to set the diameter of the progress ring.
|
||||
|
||||
```html preview
|
||||
<sl-progress-ring percentage="50" size="200"></sl-progress-ring>
|
||||
<sl-progress-ring value="50" style="--size: 200px;"></sl-progress-ring>
|
||||
```
|
||||
|
||||
### Track Width
|
||||
|
||||
Use the `track-width` attribute to set the width of the progress ring's track.
|
||||
Use the `--track-width` custom property to set the width of the progress ring's track.
|
||||
|
||||
```html preview
|
||||
<sl-progress-ring percentage="50" stroke-width="10"></sl-progress-ring>
|
||||
<sl-progress-ring value="50" style="--track-width: 10px;"></sl-progress-ring>
|
||||
```
|
||||
|
||||
### Colors
|
||||
@@ -32,7 +32,7 @@ To change the color, use the `--track-color` and `--indicator-color` custom prop
|
||||
|
||||
```html preview
|
||||
<sl-progress-ring
|
||||
percentage="50"
|
||||
value="50"
|
||||
style="
|
||||
--track-color: pink;
|
||||
--indicator-color: deeppink;
|
||||
@@ -42,10 +42,18 @@ To change the color, use the `--track-color` and `--indicator-color` custom prop
|
||||
|
||||
### Labels
|
||||
|
||||
Use the `label` attribute to label the progress ring and tell assistive devices how to announce it.
|
||||
|
||||
```html preview
|
||||
<sl-progress-ring value="50" label="Upload progress"></sl-progress-ring>
|
||||
```
|
||||
|
||||
### Showing Values
|
||||
|
||||
Use the default slot to show a label.
|
||||
|
||||
```html preview
|
||||
<sl-progress-ring percentage="50" size="200" class="progress-ring-labels" style="margin-bottom: .5rem;">50%</sl-progress-ring>
|
||||
<sl-progress-ring value="50" class="progress-ring-values" style="margin-bottom: .5rem;">50%</sl-progress-ring>
|
||||
|
||||
<br>
|
||||
|
||||
@@ -53,20 +61,20 @@ Use the default slot to show a label.
|
||||
<sl-button circle><sl-icon name="plus"></sl-icon></sl-button>
|
||||
|
||||
<script>
|
||||
const progressRing = document.querySelector('.progress-ring-labels');
|
||||
const progressRing = document.querySelector('.progress-ring-values');
|
||||
const subtractButton = progressRing.nextElementSibling.nextElementSibling;
|
||||
const addButton = subtractButton.nextElementSibling;
|
||||
|
||||
addButton.addEventListener('click', () => {
|
||||
const percentage = Math.min(100, progressRing.percentage + 10);
|
||||
progressRing.percentage = percentage;
|
||||
progressRing.textContent = `${percentage}%`;
|
||||
const value = Math.min(100, progressRing.value + 10);
|
||||
progressRing.value = value;
|
||||
progressRing.textContent = `${value}%`;
|
||||
});
|
||||
|
||||
subtractButton.addEventListener('click', () => {
|
||||
const percentage = Math.max(0, progressRing.percentage - 10)
|
||||
progressRing.percentage = percentage;
|
||||
progressRing.textContent = `${percentage}%`;
|
||||
const value = Math.max(0, progressRing.value - 10)
|
||||
progressRing.value = value;
|
||||
progressRing.textContent = `${value}%`;
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
Ranges allow the user to select a single value within a given range using a slider.
|
||||
|
||||
```html preview
|
||||
<sl-range min="0" max="100" step="1"></sl-range>
|
||||
<sl-range></sl-range>
|
||||
```
|
||||
|
||||
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form) instead.
|
||||
@@ -36,6 +36,17 @@ To disable the tooltip, set `tooltip` to `none`.
|
||||
<sl-range min="0" max="100" step="1" tooltip="none"></sl-range>
|
||||
```
|
||||
|
||||
### Custom Track Colors
|
||||
|
||||
You can customize the active and inactive portions of the track using the `--track-color-active` and `--track-color-inactive` custom properties.
|
||||
|
||||
```html preview
|
||||
<sl-range style="
|
||||
--track-color-active: rgb(var(--sl-color-primary-600));
|
||||
--track-color-inactive: rgb(var(--sl-color-primary-200));
|
||||
"></sl-range>
|
||||
```
|
||||
|
||||
### Custom Tooltip Formatter
|
||||
|
||||
You can change the tooltip's content by setting the `tooltipFormatter` property to a function that accepts the range's value as an argument.
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
[component-header:sl-resize-observer]
|
||||
|
||||
Resize observers offer a thin, declarative interface to the [`ResizeObserver API`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver).
|
||||
The Resize Observer component offers a thin, declarative interface to the [`ResizeObserver API`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver).
|
||||
|
||||
The resize observer will report changes to the dimensions of the elements it wraps through the `sl-resize` event. When emitted, a collection of [`ResizeObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry) objects will be attached to `event.detail`, containing the target element and information about its dimensions.
|
||||
The resize observer will report changes to the dimensions of the elements it wraps through the `sl-resize` event. When emitted, a collection of [`ResizeObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry) objects will be attached to `event.detail` that contains the target element and information about its dimensions.
|
||||
|
||||
```html preview
|
||||
<div class="resize-observer-overview">
|
||||
@@ -20,7 +20,7 @@ The resize observer will report changes to the dimensions of the elements it wra
|
||||
const resizeObserver = container.querySelector('sl-resize-observer');
|
||||
|
||||
resizeObserver.addEventListener('sl-resize', event => {
|
||||
console.log(event);
|
||||
console.log(event.detail);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ Selects allow you to choose one or more items from a dropdown menu.
|
||||
<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-menu-divider></sl-menu-divider>
|
||||
<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>
|
||||
@@ -44,6 +44,18 @@ Use the `clearable` attribute to make the control clearable.
|
||||
</sl-select>
|
||||
```
|
||||
|
||||
### Filled Selects
|
||||
|
||||
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-select>
|
||||
```
|
||||
|
||||
### Pill
|
||||
|
||||
Use the `pill` attribute to give selects rounded edges.
|
||||
@@ -77,7 +89,7 @@ To allow multiple options to be selected, use the `multiple` attribute. It's a g
|
||||
<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-menu-divider></sl-menu-divider>
|
||||
<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>
|
||||
@@ -86,7 +98,7 @@ To allow multiple options to be selected, use the `multiple` attribute. It's a g
|
||||
|
||||
### Grouping Options
|
||||
|
||||
Options can be grouped visually using menu labels and menu dividers.
|
||||
Options can be grouped visually using menu labels and dividers.
|
||||
|
||||
```html preview
|
||||
<sl-select placeholder="Select one">
|
||||
@@ -94,7 +106,7 @@ Options can be grouped visually using menu labels and menu dividers.
|
||||
<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-menu-divider></sl-menu-divider>
|
||||
<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>
|
||||
|
||||
@@ -34,21 +34,21 @@ Use the `pill` attribute to give tabs rounded edges.
|
||||
<sl-tag size="large" pill>Large</sl-tag>
|
||||
```
|
||||
|
||||
### Clearable
|
||||
### Removable
|
||||
|
||||
Use the `clearable` attribute to add a clear button to the tag.
|
||||
Use the `removable` attribute to add a remove button to the tag.
|
||||
|
||||
```html preview
|
||||
<div class="tags-clearable">
|
||||
<sl-tag size="small" clearable>Small</sl-tag>
|
||||
<sl-tag size="medium" clearable>Medium</sl-tag>
|
||||
<sl-tag size="large" clearable>Large</sl-tag>
|
||||
<div class="tags-removable">
|
||||
<sl-tag size="small" removable>Small</sl-tag>
|
||||
<sl-tag size="medium" removable>Medium</sl-tag>
|
||||
<sl-tag size="large" removable>Large</sl-tag>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const div = document.querySelector('.tags-clearable');
|
||||
const div = document.querySelector('.tags-removable');
|
||||
|
||||
div.addEventListener('sl-clear', event => {
|
||||
div.addEventListener('sl-remove', event => {
|
||||
const tag = event.target;
|
||||
tag.style.opacity = '0';
|
||||
setTimeout(() => tag.style.opacity = '1', 2000);
|
||||
@@ -56,7 +56,7 @@ Use the `clearable` attribute to add a clear button to the tag.
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.tags-clearable sl-tag {
|
||||
.tags-removable sl-tag {
|
||||
transition: var(--sl-transition-medium) opacity;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -30,9 +30,17 @@ Use the `placeholder` attribute to add a placeholder.
|
||||
<sl-textarea placeholder="Type something"></sl-textarea>
|
||||
```
|
||||
|
||||
### Filled Textareas
|
||||
|
||||
Add the `filled` attribute to draw a filled textarea.
|
||||
|
||||
```html preview
|
||||
<sl-textarea placeholder="Type something" filled></sl-textarea>
|
||||
```
|
||||
|
||||
### Disabled
|
||||
|
||||
Use the `disabled` attribute to disable an input.
|
||||
Use the `disabled` attribute to disable a textarea.
|
||||
|
||||
```html preview
|
||||
<sl-textarea placeholder="Textarea" disabled></sl-textarea>
|
||||
|
||||
@@ -174,4 +174,29 @@ Use the `content` slot to create tooltips with HTML content. Tooltips are design
|
||||
</sl-tooltip>
|
||||
```
|
||||
|
||||
### Hoisting
|
||||
|
||||
Tooltips will be clipped if they're inside a container that has `overflow: auto|hidden|scroll`. The `hoist` attribute forces the tooltip to use a fixed positioning strategy, allowing it to break out of the container. In this case, the tooltip will be positioned relative to its containing block, which is usually the viewport unless an ancestor uses a `transform`, `perspective`, or `filter`. [Refer to this page](https://developer.mozilla.org/en-US/docs/Web/CSS/position#fixed) for more details.
|
||||
|
||||
```html preview
|
||||
<div class="tooltip-hoist">
|
||||
<sl-tooltip content="This is a tooltip">
|
||||
<sl-button>No Hoist</sl-button>
|
||||
</sl-tooltip>
|
||||
|
||||
<sl-tooltip content="This is a tooltip" hoist>
|
||||
<sl-button>Hoist</sl-button>
|
||||
</sl-tooltip>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tooltip-hoist {
|
||||
border: solid 2px rgb(var(--sl-panel-border-color));
|
||||
overflow: hidden;
|
||||
padding: var(--sl-spacing-medium);
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
[component-metadata:sl-tooltip]
|
||||
|
||||
@@ -16,7 +16,7 @@ The easiest way to install Shoelace is with the CDN. Just add the following tags
|
||||
If you prefer to use the dark theme instead, use this. Note the `sl-theme-dark` class on the `<html>` element. [Learn more about the Dark Theme.](/getting-started/themes#dark-theme)
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/themes/light.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/themes/dark.css">
|
||||
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/shoelace.js"></script>
|
||||
```
|
||||
|
||||
@@ -79,7 +79,7 @@ However, if you're [cherry picking](#cherry-picking) or [bundling](#bundling) Sh
|
||||
|
||||
The previous approach is the _easiest_ way to load Shoelace, but easy isn't always efficient. You'll incur the full size of the library even if you only use a handful of components. This is convenient for prototyping, but may result in longer load times in production. To improve this, you can cherry pick the components you need.
|
||||
|
||||
Cherry picking can be done from your local install or [directly from the CDN](https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/). This will limit the number of files the browser has to download and reduce the amount of bytes being transferred. The disadvantage is that you need to load and register each component manually.
|
||||
Cherry picking can be done from your local install or [directly from the CDN](https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/). This will limit the number of files the browser has to download and reduce the amount of bytes being transferred. The disadvantage is that you need to load component manually.
|
||||
|
||||
Here's an example that loads only the button component. Again, if you're not using a module resolver, you'll need to adjust the path to point to the folder Shoelace is in.
|
||||
|
||||
|
||||
@@ -19,12 +19,12 @@ Designed in New Hampshire by [Cory LaViska](https://twitter.com/claviska).
|
||||
</div>
|
||||
</div>
|
||||
|
||||
[](https://www.jsdelivr.com/package/npm/@shoelace-style/shoelace)
|
||||
[](https://www.npmjs.com/package/@shoelace-style/shoelace)
|
||||
[](https://github.com/shoelace-style/shoelace/blob/next/LICENSE.md)<br>
|
||||
[](https://discord.gg/mg8f26C)
|
||||
[](https://twitter.com/shoelace_style)
|
||||
[](https://github.com/sponsors/claviska)
|
||||
[](https://www.jsdelivr.com/package/npm/@shoelace-style/shoelace)
|
||||
[](https://www.npmjs.com/package/@shoelace-style/shoelace)
|
||||
[](https://github.com/shoelace-style/shoelace/blob/next/LICENSE.md)<br>
|
||||
[](https://discord.gg/mg8f26C)
|
||||
[](https://twitter.com/shoelace_style)
|
||||
[](https://github.com/shoelace-style/shoelace)
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
||||
@@ -108,7 +108,9 @@ You will, however, need to maintain your theme more carefully, as new versions o
|
||||
|
||||
## Dark Theme
|
||||
|
||||
The built-in dark theme uses an inverted + shifted color scale so, if you're using design tokens as intended, you'll get a decent dark mode for free. While this isn't the same as a professionally curated dark theme, it provides an excellent baseline for one and you're encouraged to customize it further depending on your needs.
|
||||
The built-in dark theme uses an inverted + shifted color scale so, if you're using design tokens as intended, you'll get a decent dark mode for free. While this isn't the same as a professionally curated dark theme, it provides an excellent baseline for one and you're encouraged to customize it depending on your needs.
|
||||
|
||||
This was achieved by taking the light theme's [color tokens](/tokens/color) and "flipping" the scale so 100 becomes 900, 200 becomes 800, 300 becomes 700, etc. Next, the luminance of each primitive was increased slightly to avoid true black, a color that is typically undesirable in dark themes. The result is a custom palette that complements the light theme well and makes it easy to offer light and dark variations with minimal effort.
|
||||
|
||||
To install the dark theme, add the following to the `<head>` section of your page.
|
||||
|
||||
|
||||
@@ -168,9 +168,14 @@ const MyComponent = (props) => {
|
||||
Vue [plays nice](https://custom-elements-everywhere.com/#vue) with custom elements. You just have to tell it to ignore Shoelace components. This is pretty easy because they all start with `sl-`.
|
||||
|
||||
```js
|
||||
Vue.config.ignoredElements = [/^sl-/];
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
|
||||
new Vue({ ... });
|
||||
const app = createApp(App);
|
||||
|
||||
app.config.compilerOptions.isCustomElement = tag => tag.startsWith('sl-');
|
||||
|
||||
app.mount('#app');
|
||||
```
|
||||
|
||||
### Binding Complex Data
|
||||
@@ -197,7 +202,7 @@ If that's too verbose, you can use a custom directive instead.
|
||||
|
||||
### Using a Custom Directive
|
||||
|
||||
You can use [this utility](https://www.npmjs.com/package/@shoelace-style/vue-sl-model) to add a custom directive to Vue that will work just like `v-model` but for Shoelace components. To install it, use this command.
|
||||
You can use [this utility](https://www.npmjs.com/package/@shoelace-style/vue-sl-model) to add a custom directive that will work just like `v-model` but for Shoelace components. To install it, use this command.
|
||||
|
||||
```bash
|
||||
npm install @shoelace-style/vue-sl-model
|
||||
@@ -207,12 +212,15 @@ Next, import the directive and enable it like this.
|
||||
|
||||
```js
|
||||
import ShoelaceModelDirective from '@shoelace-style/vue-sl-model';
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
|
||||
Vue.config.ignoredElements = [/^sl-/];
|
||||
Vue.use(ShoelaceModelDirective);
|
||||
const app = createApp(App);
|
||||
app.use(ShoelaceModelDirective);
|
||||
|
||||
// Your init here
|
||||
new Vue({ ... });
|
||||
app.config.compilerOptions.isCustomElement = tag => tag.startsWith('sl-');
|
||||
|
||||
app.mount('#app');
|
||||
```
|
||||
|
||||
Now you can use the `v-sl-model` directive to keep your data in sync!
|
||||
|
||||
@@ -6,6 +6,78 @@ Components with the <sl-badge type="warning" pill>Experimental</sl-badge> badge
|
||||
|
||||
_During the beta period, these restrictions may be relaxed in the event of a mission-critical bug._ 🐛
|
||||
|
||||
## Next
|
||||
|
||||
- Added experimental `<sl-context-menu>` component
|
||||
- Added eye dropper to `<sl-color-picker>` when the browser supports the [EyeDropper API](https://wicg.github.io/eyedropper-api/)
|
||||
- Fixed a bug in `<sl-button-group>` where buttons groups with only one button would have an incorrect border radius
|
||||
- Improved the `<sl-color-picker>` trigger's border in dark mode
|
||||
- Refactored positioning logic in `<sl-dropdown>` so Popper is only active when the menu is open
|
||||
- Updated to Lit 2.0.2
|
||||
|
||||
## 2.0.0-beta.58
|
||||
|
||||
This version once again restores the bundled distribution because the unbundled + CDN approach is currently confusing and [not working properly](https://github.com/shoelace-style/shoelace/issues/559#issuecomment-949662331). Unbundling the few dependencies Shoelace has is still a goal of the project, but [this jsDelivr bug](https://github.com/jsdelivr/jsdelivr/issues/18337) needs to be resolved before we can achieve it.
|
||||
|
||||
I sincerely apologize for the instability of the last few beta releases as a result of this effort.
|
||||
|
||||
- Added experimental `<sl-animated-image>` component
|
||||
- Added `label` attribute to `<sl-progress-bar>` and `<sl-progress-ring>` to improve a11y
|
||||
- Fixed a bug where the tooltip would show briefly when clicking a disabled `<sl-range>`
|
||||
- Fixed a bug that caused a console error when `<sl-range>` was used
|
||||
- Fixed a bug where the `nav` part in `<sl-tab-group>` was on the incorrect element [#563](https://github.com/shoelace-style/shoelace/pull/563)
|
||||
- Fixed a bug where non-integer aspect ratios were calculated incorrectly in `<sl-responsive-media>`
|
||||
- Fixed a bug in `<sl-range>` where setting `value` wouldn't update the active and inactive portion of the track [#572](https://github.com/shoelace-style/shoelace/pull/572)
|
||||
- Reverted to publishing the bundled dist and removed `/+esm` links from the docs
|
||||
- Updated to Bootstrap Icons to 1.6.1
|
||||
|
||||
## 2.0.0-beta.57
|
||||
|
||||
- Fix CodePen links and CDN links
|
||||
|
||||
## 2.0.0-beta.56
|
||||
|
||||
This release is the second attempt at unbundling dependencies. This will be a breaking change only if your configuration _does not_ support bare module specifiers. CDN users and bundler users will be unaffected, but note the URLs for modules on the CDN must have the `/+esm` now.
|
||||
|
||||
- Added the `hoist` attribute to `<sl-tooltip>` [#564](https://github.com/shoelace-style/shoelace/issues/564)
|
||||
- Unbundled dependencies and configured external imports to be packaged with bare module specifiers
|
||||
|
||||
## 2.0.0-beta.55
|
||||
|
||||
- Revert unbundling due to issues with the CDN not handling bare module specifiers as expected
|
||||
|
||||
## 2.0.0-beta.54
|
||||
|
||||
Shoelace doesn't have a lot of dependencies, but this release unbundles most of them so you can potentially save some extra kilobytes. This will be a breaking change only if your configuration _does not_ support bare module specifiers. CDN users and bundler users will be unaffected.
|
||||
|
||||
- 🚨 BREAKING: renamed the `sl-clear` event to `sl-remove`, the `clear-button` part to `remove-button`, and the `clearable` property to `removable` in `<sl-tag>`
|
||||
- Added the `disabled` prop to `<sl-resize-observer>`
|
||||
- Fixed a bug in `<sl-mutation-observer>` where setting `disabled` initially didn't work
|
||||
- Unbundled dependencies and configured external imports to be packaged with bare module specifiers
|
||||
|
||||
## 2.0.0-beta.53
|
||||
|
||||
- 🚨 BREAKING: removed `<sl-menu-divider>` (use `<sl-divider>` instead)
|
||||
- 🚨 BREAKING: removed `percentage` attribute from `<sl-progress-bar>` and `<sl-progress-ring>` (use `value`) instead
|
||||
- 🚨 BREAKING: switched the default `type` of `<sl-tag>` from `primary` to `neutral`
|
||||
- Added the experimental `<sl-mutation-observer>` component
|
||||
- Added the `<sl-divider>` component
|
||||
- Added `--sl-surface-base` and `--sl-surface-base-alt` as early surface tokens to improve the appearance of alert, card, and panels in dark mode
|
||||
- Added the `--sl-panel-border-width` design token
|
||||
- Added missing background color to `<sl-details>`
|
||||
- Added the `--padding` custom property to `<sl-tab-panel>`
|
||||
- Added the `outline` variation to `<sl-button>` [#522](https://github.com/shoelace-style/shoelace/issues/522)
|
||||
- Added the `filled` variation to `<sl-input>`, `<sl-textarea>`, and `<sl-select>` and supporting design tokens
|
||||
- Added the `control` part to `<sl-select>` so you can target the main control with CSS [#538](https://github.com/shoelace-style/shoelace/issues/538)
|
||||
- Added a border to `<sl-badge>` to improve contrast when drawn on various background colors
|
||||
- Added `--track-color-active` and `--track-color-inactive` custom properties to `<sl-range>` [#550](https://github.com/shoelace-style/shoelace/issues/550)
|
||||
- Added the undocumented custom properties `--thumb-size`, `--tooltip-offset`, `--track-height` on `<sl-range>`
|
||||
- Changed the default `distance` in `<sl-dropdown>` from `2` to `0` [#538](https://github.com/shoelace-style/shoelace/issues/538)
|
||||
- Fixed a bug where `<sl-select>` would be larger than the viewport when it had lots of options [#544](https://github.com/shoelace-style/shoelace/issues/544)
|
||||
- Fixed a bug where `<sl-progress-ring>` wouldn't animate in Safari
|
||||
- Updated the default height of `<sl-progress-bar>` from `16px` to `1rem` and added a subtle shadow to indicate depth
|
||||
- Removed the `lit-html` dependency and moved corresponding imports to `lit` [#546](https://github.com/shoelace-style/shoelace/issues/546)
|
||||
|
||||
## 2.0.0-beta.52
|
||||
|
||||
- 🚨 BREAKING: changed the `--stroke-width` custom property of `<sl-spinner>` to `--track-width` for consistency
|
||||
|
||||
@@ -14,6 +14,7 @@ The [discussion forum](https://github.com/shoelace-style/shoelace/discussions) i
|
||||
- Learn more about the project, its values, and its roadmap
|
||||
|
||||
<sl-button type="primary" href="https://github.com/shoelace-style/shoelace/discussions" target="_blank">
|
||||
<sl-icon name="github" slot="prefix"></sl-icon>
|
||||
Join the Discussion
|
||||
</sl-button>
|
||||
|
||||
@@ -27,9 +28,19 @@ The [community chat](https://discord.gg/mg8f26C) is open to the public and power
|
||||
- Chat live with other designers, developers, and Shoelace fans
|
||||
|
||||
<sl-button type="primary" href="https://discord.gg/mg8f26C" target="_blank">
|
||||
<sl-icon name="discord" slot="prefix"></sl-icon>
|
||||
Join the Chat
|
||||
</sl-button>
|
||||
|
||||
## Stack Overflow
|
||||
|
||||
You can post questions on Stack Overflow using [the "shoelace" tag](https://stackoverflow.com/questions/tagged/shoelace). This is a public forum where talented developers answer questions. It's a great way to get help, but it is not maintained or actively monitored by the Shoelace author.
|
||||
|
||||
<sl-button type="primary" href="https://stackoverflow.com/questions/ask?tags=shoelace" target="_blank">
|
||||
<sl-icon name="stack-overflow" slot="prefix"></sl-icon>
|
||||
Ask for Help
|
||||
</sl-button>
|
||||
|
||||
## Twitter
|
||||
|
||||
Follow [@shoelace_style](https://twitter.com/shoelace_style) on Twitter for general updates and announcements about Shoelace. This is a great place to say "hi" or to share something you're working on. You're also welcome to follow [@claviska](https://twitter.com/claviska), the creator, for tweets about web components, web development, and life.
|
||||
@@ -37,5 +48,6 @@ Follow [@shoelace_style](https://twitter.com/shoelace_style) on Twitter for gene
|
||||
**Please avoid using Twitter for support questions.** The [discussion forum](https://github.com/shoelace-style/shoelace/discussions) is a much better place to share code snippets, screenshots, and other troubleshooting info. You'll have much better luck there, as more users will have a chance to help you.
|
||||
|
||||
<sl-button type="primary" href="https://twitter.com/shoelace_style" target="_blank">
|
||||
<sl-icon name="twitter" slot="prefix"></sl-icon>
|
||||
Follow on Twitter
|
||||
</sl-button>
|
||||
|
||||
114
docs/tutorials/integrating-with-laravel.md
Normal file
114
docs/tutorials/integrating-with-laravel.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Integrating with Laravel
|
||||
|
||||
This page explains how to integrate Shoelace with a [Laravel](https://laravel.com) app using a local Webpack bundle. This is a community-maintained document. For questions about this integration, please [ask the community](/resources/community).
|
||||
|
||||
## Requirements
|
||||
|
||||
This integration has been tested with the following:
|
||||
|
||||
- Laravel >= 8
|
||||
- Node >= 14
|
||||
- Laravel Mix >= 6
|
||||
|
||||
## Instructions
|
||||
|
||||
These instructions assume an out-of-the-box [Laravel 8+ install](https://laravel.com/docs/8.x/installation) that uses [Laravel Mix](https://laravel.com/docs/8.x/mix) to compile assets.
|
||||
Be sure to run `npm install` to install the default Laravel front-end dependencies before installing Shoelace.
|
||||
|
||||
### Install the Shoelace package
|
||||
|
||||
```bash
|
||||
npm install @shoelace-style/shoelace
|
||||
```
|
||||
|
||||
### Import the Default Theme
|
||||
|
||||
Import Shoelace's default theme (stylesheet) in `/resources/css/app.css`:
|
||||
|
||||
```css
|
||||
@import "/node_modules/@shoelace-style/shoelace/dist/themes/light.css";
|
||||
```
|
||||
|
||||
### Import Your Shoelace Components
|
||||
|
||||
Import each Shoelace component you plan to use in `/resources/js/boostrap.js`. Since [Laravel Mix](https://laravel.com/docs/8.x/mix) uses Webpack, use the full path to each component -- as outlined in the [Cherry Picking instructions](https://shoelace.style/getting-started/installation?id=cherry-picking). You can find the full import statement for a component in the *Importing* section of the component's documentation (use the *Bundler* import). Your imports should look similar to:
|
||||
|
||||
```js
|
||||
import "@shoelace-style/shoelace/dist/components/button/button.js";
|
||||
import "@shoelace-style/shoelace/dist/components/dialog/dialog.js";
|
||||
import "@shoelace-style/shoelace/dist/components/drawer/drawer.js";
|
||||
import "@shoelace-style/shoelace/dist/components/menu/menu.js";
|
||||
import "@shoelace-style/shoelace/dist/components/menu-item/menu-item.js";
|
||||
```
|
||||
|
||||
### Set the Base Path
|
||||
|
||||
Add the base path to your Shoelace assets (icons, images, etc.) in `/resources/js/boostrap.js`. The path must point to the same folder where you copy assets to in the next step.
|
||||
|
||||
```js
|
||||
import { setBasePath } from "@shoelace-style/shoelace/dist/utilities/base-path.js";
|
||||
setBasePath("/");
|
||||
```
|
||||
|
||||
Here's an example `/resources/js/boostrap.js` file, after importing and setting the base path and components.
|
||||
|
||||
```js
|
||||
import { setBasePath } from "@shoelace-style/shoelace/dist/utilities/base-path.js";
|
||||
setBasePath("/assets");
|
||||
|
||||
import "@shoelace-style/shoelace/dist/components/button/button.js";
|
||||
import "@shoelace-style/shoelace/dist/components/dialog/dialog.js";
|
||||
import "@shoelace-style/shoelace/dist/components/drawer/drawer.js";
|
||||
import "@shoelace-style/shoelace/dist/components/menu/menu.js";
|
||||
import "@shoelace-style/shoelace/dist/components/menu-item/menu-item.js";
|
||||
```
|
||||
|
||||
|
||||
### Configure Laravel Mix
|
||||
|
||||
[Laravel Mix](https://laravel.com/docs/8.x/mix) is a wrapper around Webpack that simplifies configuration. Mix is used by default for compiling front-end assets in Laravel.
|
||||
|
||||
Modify `webpack.mix.js` to add Shoelace's assets to Webpack's build process:
|
||||
```js
|
||||
mix.js("resources/js/app.js", "public/js")
|
||||
.postCss("resources/css/app.css", "public/css", [])
|
||||
.copy("node_modules/@shoelace-style/shoelace/dist/assets", "public/assets")
|
||||
```
|
||||
|
||||
Consider [extracting vendor libraries](https://laravel.com/docs/8.x/mix#vendor-extraction) to a separate file. This splits frequently updated vendor libraries (like Shoelace) from your front-end application code -- for better long-term caching.
|
||||
Here's an example `webpack.mix.js` file that compiles and splits your JS into `app.js` and `vendor.js` files, and builds an optimized CSS bundle using PostCSS.
|
||||
|
||||
```js
|
||||
mix.js("resources/js/app.js", "public/js")
|
||||
.postCss("resources/css/app.css", "public/css", [])
|
||||
.copy("node_modules/@shoelace-style/shoelace/dist/assets", "public/assets")
|
||||
.extract(); // extracts libraries in node_modules to vendor.js
|
||||
```
|
||||
|
||||
### Compile Front-End Assets
|
||||
|
||||
Run the [Laravel Mix](https://laravel.com/docs/8.x/mix) npm scripts to build your application's CSS and JavaScript code.
|
||||
|
||||
```bash
|
||||
## build a development bundle
|
||||
npm run dev
|
||||
|
||||
## build a production bundle
|
||||
npm run prod
|
||||
```
|
||||
|
||||
### Include Front-End Assets in Your Layout File
|
||||
|
||||
Most full-stack Laravel applications use [layouts](https://laravel.com/docs/8.x/blade#building-layouts) to define the basic structure of a page.
|
||||
After compiling your front-end assets (above), include them in your top-level layouts/templates. The following example uses the [Laravel asset helper](https://laravel.com/docs/8.x/helpers#method-asset) to generate a full URL.
|
||||
|
||||
```html
|
||||
<script defer src="{{ asset('js/manifest.js') }}"></script>
|
||||
<script defer src="{{ asset('js/vendor.js') }}"></script>
|
||||
<script defer src="{{ asset('/js/app.js') }}"></script>
|
||||
|
||||
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
|
||||
```
|
||||
|
||||
Have fun using Shoelace components in your Laravel app!
|
||||
|
||||
@@ -23,9 +23,12 @@ yarn add @shoelace-style/shoelace copy-webpack-plugin
|
||||
The next step is to import Shoelace's default theme (stylesheet) in `app/javascript/stylesheets/application.scss`.
|
||||
|
||||
```css
|
||||
@import '~@shoelace-style/shoelace/dist/themes/base';
|
||||
@import '~@shoelace-style/shoelace/dist/themes/light';
|
||||
@import '~@shoelace-style/shoelace/dist/themes/dark'; // Optional dark theme
|
||||
```
|
||||
|
||||
Fore more details about themes, please refer to [Theme Basics](/getting-started/themes?id=theme-basics).
|
||||
|
||||
### Importing Required Scripts
|
||||
|
||||
After importing the theme, you'll need to import the JavaScript files for Shoelace. Add the following code to `app/javascript/packs/application.js`.
|
||||
|
||||
69
package-lock.json
generated
69
package-lock.json
generated
@@ -1,19 +1,17 @@
|
||||
{
|
||||
"name": "@shoelace-style/shoelace",
|
||||
"version": "2.0.0-beta.52",
|
||||
"version": "2.0.0-beta.58",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@shoelace-style/shoelace",
|
||||
"version": "2.0.0-beta.52",
|
||||
"version": "2.0.0-beta.58",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.7.0",
|
||||
"@shoelace-style/animations": "^1.1.0",
|
||||
"color": "^3.1.3",
|
||||
"lit": "^2.0.0",
|
||||
"lit-html": "^2.0.0",
|
||||
"qr-creator": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -24,10 +22,10 @@
|
||||
"@web/test-runner": "^0.13.5",
|
||||
"@web/test-runner-puppeteer": "^0.10.0",
|
||||
"bluebird": "^3.7.2",
|
||||
"bootstrap-icons": "^1.4.1",
|
||||
"bootstrap-icons": "^1.6.1",
|
||||
"browser-sync": "^2.26.14",
|
||||
"chalk": "^4.1.0",
|
||||
"command-line-args": "^5.1.1",
|
||||
"command-line-args": "^5.2.0",
|
||||
"comment-parser": "^1.1.5",
|
||||
"concurrently": "^5.3.0",
|
||||
"del": "^6.0.0",
|
||||
@@ -37,6 +35,7 @@
|
||||
"get-port": "^5.1.1",
|
||||
"globby": "^11.0.4",
|
||||
"husky": "^4.3.8",
|
||||
"lit": "^2.0.2",
|
||||
"lunr": "^2.3.9",
|
||||
"mkdirp": "^0.5.5",
|
||||
"plop": "^2.7.4",
|
||||
@@ -177,7 +176,8 @@
|
||||
"node_modules/@lit/reactive-element": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.0.0.tgz",
|
||||
"integrity": "sha512-Kpgenb8UNFsKCsFhggiVvUkCbcFQSd6N8hffYEEGjz27/4rw3cTSsmP9t3q1EHOAsdum60Wo64HvuZDFpEwexA=="
|
||||
"integrity": "sha512-Kpgenb8UNFsKCsFhggiVvUkCbcFQSd6N8hffYEEGjz27/4rw3cTSsmP9t3q1EHOAsdum60Wo64HvuZDFpEwexA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@mdn/browser-compat-data": {
|
||||
"version": "3.3.5",
|
||||
@@ -802,7 +802,8 @@
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
|
||||
"integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg=="
|
||||
"integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/uuid": {
|
||||
"version": "8.3.0",
|
||||
@@ -1664,9 +1665,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/bootstrap-icons": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.5.0.tgz",
|
||||
"integrity": "sha512-44feMc7DE1Ccpsas/1wioN8ewFJNquvi5FewA06wLnqct7CwMdGDVy41ieHaacogzDqLfG8nADIvMNp9e4bfbA==",
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.6.1.tgz",
|
||||
"integrity": "sha512-MNpF89+njCdVJePDRbCd2DrUusqIyNsPlBrdKqBEXAvFZpwb+Gc8k2VlyF2ueiDQn1PoeTSg9UqQNgx8tGqHAA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -2426,12 +2427,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/command-line-args": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.1.1.tgz",
|
||||
"integrity": "sha512-hL/eG8lrll1Qy1ezvkant+trihbGnaKaeEjj6Scyr3DN+RC7iQ5Rz84IeLERfAWDGo0HBSNAakczwgCilDXnWg==",
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.0.tgz",
|
||||
"integrity": "sha512-4zqtU1hYsSJzcJBOcNZIbW5Fbk9BkjCp1pZVhQKoRaWL5J7N4XphDLwo8aWwdQpTugxwu+jf9u2ZhkXiqp5Z6A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"array-back": "^3.0.1",
|
||||
"array-back": "^3.1.0",
|
||||
"find-replace": "^3.0.0",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"typical": "^4.0.0"
|
||||
@@ -6145,9 +6146,10 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lit": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-2.0.0.tgz",
|
||||
"integrity": "sha512-pqi5O/wVzQ9Bn4ERRoYQlt1EAUWyY5Wv888vzpoArbtChc+zfUv1XohRqSdtQZYCogl0eHKd+MQwymg2XJfECg==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-2.0.2.tgz",
|
||||
"integrity": "sha512-hKA/1YaSB+P+DvKWuR2q1Xzy/iayhNrJ3aveD0OQ9CKn6wUjsdnF/7LavDOJsKP/K5jzW/kXsuduPgRvTFrFJw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^1.0.0",
|
||||
"lit-element": "^3.0.0",
|
||||
@@ -6158,6 +6160,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.0.0.tgz",
|
||||
"integrity": "sha512-oPqRhhBBhs+AlI62QLwtWQNU/bNK/h2L1jI3IDroqZubo6XVAkyNy2dW3CRfjij8mrNlY7wULOfyyKKOnfEePA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@lit/reactive-element": "^1.0.0",
|
||||
"lit-html": "^2.0.0"
|
||||
@@ -6167,6 +6170,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.0.0.tgz",
|
||||
"integrity": "sha512-tJsCapCmc0vtLj6harqd6HfCxnlt/RSkgowtz4SC9dFE3nSL38Tb33I5HMDiyJsRjQZRTgpVsahrnDrR9wg27w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/trusted-types": "^2.0.2"
|
||||
}
|
||||
@@ -11292,7 +11296,8 @@
|
||||
"@lit/reactive-element": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.0.0.tgz",
|
||||
"integrity": "sha512-Kpgenb8UNFsKCsFhggiVvUkCbcFQSd6N8hffYEEGjz27/4rw3cTSsmP9t3q1EHOAsdum60Wo64HvuZDFpEwexA=="
|
||||
"integrity": "sha512-Kpgenb8UNFsKCsFhggiVvUkCbcFQSd6N8hffYEEGjz27/4rw3cTSsmP9t3q1EHOAsdum60Wo64HvuZDFpEwexA==",
|
||||
"dev": true
|
||||
},
|
||||
"@mdn/browser-compat-data": {
|
||||
"version": "3.3.5",
|
||||
@@ -11900,7 +11905,8 @@
|
||||
"@types/trusted-types": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
|
||||
"integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg=="
|
||||
"integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/uuid": {
|
||||
"version": "8.3.0",
|
||||
@@ -12573,9 +12579,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"bootstrap-icons": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.5.0.tgz",
|
||||
"integrity": "sha512-44feMc7DE1Ccpsas/1wioN8ewFJNquvi5FewA06wLnqct7CwMdGDVy41ieHaacogzDqLfG8nADIvMNp9e4bfbA==",
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.6.1.tgz",
|
||||
"integrity": "sha512-MNpF89+njCdVJePDRbCd2DrUusqIyNsPlBrdKqBEXAvFZpwb+Gc8k2VlyF2ueiDQn1PoeTSg9UqQNgx8tGqHAA==",
|
||||
"dev": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
@@ -13210,12 +13216,12 @@
|
||||
}
|
||||
},
|
||||
"command-line-args": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.1.1.tgz",
|
||||
"integrity": "sha512-hL/eG8lrll1Qy1ezvkant+trihbGnaKaeEjj6Scyr3DN+RC7iQ5Rz84IeLERfAWDGo0HBSNAakczwgCilDXnWg==",
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.0.tgz",
|
||||
"integrity": "sha512-4zqtU1hYsSJzcJBOcNZIbW5Fbk9BkjCp1pZVhQKoRaWL5J7N4XphDLwo8aWwdQpTugxwu+jf9u2ZhkXiqp5Z6A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"array-back": "^3.0.1",
|
||||
"array-back": "^3.1.0",
|
||||
"find-replace": "^3.0.0",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"typical": "^4.0.0"
|
||||
@@ -16167,9 +16173,10 @@
|
||||
"dev": true
|
||||
},
|
||||
"lit": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-2.0.0.tgz",
|
||||
"integrity": "sha512-pqi5O/wVzQ9Bn4ERRoYQlt1EAUWyY5Wv888vzpoArbtChc+zfUv1XohRqSdtQZYCogl0eHKd+MQwymg2XJfECg==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-2.0.2.tgz",
|
||||
"integrity": "sha512-hKA/1YaSB+P+DvKWuR2q1Xzy/iayhNrJ3aveD0OQ9CKn6wUjsdnF/7LavDOJsKP/K5jzW/kXsuduPgRvTFrFJw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@lit/reactive-element": "^1.0.0",
|
||||
"lit-element": "^3.0.0",
|
||||
@@ -16180,6 +16187,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.0.0.tgz",
|
||||
"integrity": "sha512-oPqRhhBBhs+AlI62QLwtWQNU/bNK/h2L1jI3IDroqZubo6XVAkyNy2dW3CRfjij8mrNlY7wULOfyyKKOnfEePA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@lit/reactive-element": "^1.0.0",
|
||||
"lit-html": "^2.0.0"
|
||||
@@ -16189,6 +16197,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.0.0.tgz",
|
||||
"integrity": "sha512-tJsCapCmc0vtLj6harqd6HfCxnlt/RSkgowtz4SC9dFE3nSL38Tb33I5HMDiyJsRjQZRTgpVsahrnDrR9wg27w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/trusted-types": "^2.0.2"
|
||||
}
|
||||
|
||||
13
package.json
13
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@shoelace-style/shoelace",
|
||||
"description": "A forward-thinking library of web components.",
|
||||
"version": "2.0.0-beta.52",
|
||||
"version": "2.0.0-beta.58",
|
||||
"homepage": "https://github.com/shoelace-style/shoelace",
|
||||
"author": "Cory LaViska",
|
||||
"license": "MIT",
|
||||
@@ -30,8 +30,8 @@
|
||||
"url": "https://github.com/sponsors/claviska"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node scripts/build.js --dev",
|
||||
"build": "node scripts/build.js",
|
||||
"start": "node scripts/build.js --bundle --serve",
|
||||
"build": "node scripts/build.js --bundle --types --copydir \"docs/dist\"",
|
||||
"prepublishOnly": "npm run build && npm run test",
|
||||
"prettier": "prettier --write --loglevel warn .",
|
||||
"create": "plop --plopfile scripts/plop/plopfile.cjs",
|
||||
@@ -42,8 +42,6 @@
|
||||
"@popperjs/core": "^2.7.0",
|
||||
"@shoelace-style/animations": "^1.1.0",
|
||||
"color": "^3.1.3",
|
||||
"lit": "^2.0.0",
|
||||
"lit-html": "^2.0.0",
|
||||
"qr-creator": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -54,10 +52,10 @@
|
||||
"@web/test-runner": "^0.13.5",
|
||||
"@web/test-runner-puppeteer": "^0.10.0",
|
||||
"bluebird": "^3.7.2",
|
||||
"bootstrap-icons": "^1.4.1",
|
||||
"bootstrap-icons": "^1.6.1",
|
||||
"browser-sync": "^2.26.14",
|
||||
"chalk": "^4.1.0",
|
||||
"command-line-args": "^5.1.1",
|
||||
"command-line-args": "^5.2.0",
|
||||
"comment-parser": "^1.1.5",
|
||||
"concurrently": "^5.3.0",
|
||||
"del": "^6.0.0",
|
||||
@@ -67,6 +65,7 @@
|
||||
"get-port": "^5.1.1",
|
||||
"globby": "^11.0.4",
|
||||
"husky": "^4.3.8",
|
||||
"lit": "^2.0.2",
|
||||
"lunr": "^2.3.9",
|
||||
"mkdirp": "^0.5.5",
|
||||
"plop": "^2.7.4",
|
||||
|
||||
105
scripts/build.js
105
scripts/build.js
@@ -1,6 +1,3 @@
|
||||
//
|
||||
// Builds the project. To spin up a dev server, pass the --serve flag.
|
||||
//
|
||||
import browserSync from 'browser-sync';
|
||||
import chalk from 'chalk';
|
||||
import commandLineArgs from 'command-line-args';
|
||||
@@ -10,52 +7,67 @@ import esbuild from 'esbuild';
|
||||
import fs from 'fs';
|
||||
import getPort from 'get-port';
|
||||
import glob from 'globby';
|
||||
import mkdirp from 'mkdirp';
|
||||
import path from 'path';
|
||||
import { URL } from 'url';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
const build = esbuild.build;
|
||||
const bs = browserSync.create();
|
||||
const { dev } = commandLineArgs({ name: 'dev', type: Boolean });
|
||||
|
||||
del.sync('./dist');
|
||||
const { bundle, copydir, dir, serve, types } = commandLineArgs([
|
||||
{ name: 'bundle', type: Boolean },
|
||||
{ name: 'copydir', type: String },
|
||||
{ name: 'dir', type: String, defaultValue: 'dist' },
|
||||
{ name: 'serve', type: Boolean },
|
||||
{ name: 'types', type: Boolean }
|
||||
]);
|
||||
|
||||
try {
|
||||
if (!dev) execSync('tsc', { stdio: 'inherit' }); // for type declarations
|
||||
execSync('node scripts/make-metadata.js', { stdio: 'inherit' });
|
||||
execSync('node scripts/make-search.js', { stdio: 'inherit' });
|
||||
execSync('node scripts/make-vscode-data.js', { stdio: 'inherit' });
|
||||
execSync('node scripts/make-css.js', { stdio: 'inherit' });
|
||||
execSync('node scripts/make-icons.js', { stdio: 'inherit' });
|
||||
} catch (err) {
|
||||
console.error(chalk.red(err));
|
||||
process.exit(1);
|
||||
}
|
||||
const outdir = dir;
|
||||
|
||||
del.sync(outdir);
|
||||
mkdirp.sync(outdir);
|
||||
|
||||
(async () => {
|
||||
const entryPoints = [
|
||||
// The whole shebang dist
|
||||
'./src/shoelace.ts',
|
||||
// Components
|
||||
...(await glob('./src/components/**/!(*.(style|test)).ts')),
|
||||
// Public utilities
|
||||
...(await glob('./src/utilities/**/!(*.(style|test)).ts')),
|
||||
// Theme stylesheets
|
||||
...(await glob('./src/themes/**/!(*.test).ts'))
|
||||
];
|
||||
try {
|
||||
if (types) execSync(`tsc --project . --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-metadata.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-search.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-vscode-data.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-css.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-icons.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
} catch (err) {
|
||||
console.error(chalk.red(err));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const buildResult = await esbuild
|
||||
.build({
|
||||
format: 'esm',
|
||||
target: 'es2017',
|
||||
entryPoints,
|
||||
outdir: './dist',
|
||||
entryPoints: [
|
||||
// The whole shebang
|
||||
'./src/shoelace.ts',
|
||||
// Components
|
||||
...(await glob('./src/components/**/!(*.(style|test)).ts')),
|
||||
// Public utilities
|
||||
...(await glob('./src/utilities/**/!(*.(style|test)).ts')),
|
||||
// Theme stylesheets
|
||||
...(await glob('./src/themes/**/!(*.test).ts'))
|
||||
],
|
||||
outdir,
|
||||
chunkNames: 'chunks/[name].[hash]',
|
||||
incremental: dev,
|
||||
incremental: serve,
|
||||
define: {
|
||||
// Popper.js expects this to be set
|
||||
'process.env.NODE_ENV': '"production"'
|
||||
},
|
||||
bundle: true,
|
||||
//
|
||||
// We don't bundle certain dependencies in the unbundled build. This ensures we ship bare module specifiers,
|
||||
// allowing end users to better optimize when using a bundler. (Only packages that ship ESM can be external.)
|
||||
//
|
||||
external: bundle ? undefined : ['@popperjs/core', '@shoelace-style/animations', 'lit', 'qr-creator'],
|
||||
splitting: true,
|
||||
plugins: []
|
||||
})
|
||||
@@ -64,20 +76,23 @@ try {
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Create the docs distribution by copying dist into the docs folder. This is what powers the website. It doesn't need
|
||||
// to exist in dev because Browser Sync routes it virtually.
|
||||
await del('./docs/dist');
|
||||
if (!dev) {
|
||||
await Promise.all([copy('./dist', './docs/dist')]);
|
||||
// Copy the build output to an additional directory
|
||||
if (copydir) {
|
||||
del.sync(copydir);
|
||||
copy(outdir, copydir);
|
||||
}
|
||||
|
||||
console.log(chalk.green('The build has finished! 📦\n'));
|
||||
console.log(chalk.green(`The build has been generated at ${outdir} 📦\n`));
|
||||
|
||||
if (dev) {
|
||||
// Dev server
|
||||
if (serve) {
|
||||
const port = await getPort({
|
||||
port: getPort.makeRange(4000, 4999)
|
||||
});
|
||||
|
||||
// Make sure docs/dist is empty since we're serving it virtually
|
||||
del.sync('docs/dist');
|
||||
|
||||
console.log(chalk.cyan(`Launching the Shoelace dev server at http://localhost:${port}! 🥾\n`));
|
||||
|
||||
// Launch browser sync
|
||||
@@ -104,10 +119,10 @@ try {
|
||||
buildResult
|
||||
// Rebuild and reload
|
||||
.rebuild()
|
||||
.then(async () => {
|
||||
.then(() => {
|
||||
// Rebuild stylesheets when a theme file changes
|
||||
if (/^src\/themes/.test(filename)) {
|
||||
execSync('node scripts/make-css.js', { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-css.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
@@ -116,20 +131,22 @@ try {
|
||||
return;
|
||||
}
|
||||
|
||||
execSync('node scripts/make-metadata.js', { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-metadata.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
})
|
||||
.then(() => {
|
||||
bs.reload();
|
||||
})
|
||||
.then(() => bs.reload())
|
||||
.catch(err => console.error(chalk.red(err)));
|
||||
});
|
||||
|
||||
// Reload without rebuilding when the docs change
|
||||
bs.watch(['docs/**/*.md']).on('change', filename => {
|
||||
console.log(`Docs file changed - ${filename}`);
|
||||
execSync('node scripts/make-search.js', { stdio: 'inherit' });
|
||||
execSync(`node scripts/make-search.js --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
bs.reload();
|
||||
});
|
||||
|
||||
// Cleanup on exit
|
||||
process.on('SIGTERM', () => buildResult.rebuild.dispose());
|
||||
}
|
||||
|
||||
// Cleanup on exit
|
||||
process.on('SIGTERM', () => buildResult.rebuild.dispose());
|
||||
})();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// This script generates stylesheets from all *.styles.ts files in src/themes
|
||||
//
|
||||
import chalk from 'chalk';
|
||||
import commandLineArgs from 'command-line-args';
|
||||
import esbuild from 'esbuild';
|
||||
import fs from 'fs/promises';
|
||||
import glob from 'globby';
|
||||
@@ -10,12 +11,13 @@ import path from 'path';
|
||||
import prettier from 'prettier';
|
||||
import stripComments from 'strip-css-comments';
|
||||
|
||||
const { outdir } = commandLineArgs({ name: 'outdir', type: String });
|
||||
const files = glob.sync('./src/themes/**/*.styles.ts');
|
||||
const outdir = './dist/themes';
|
||||
const themesDir = path.join(outdir, 'themes');
|
||||
|
||||
console.log('Generating stylesheets');
|
||||
|
||||
mkdirp.sync(outdir);
|
||||
mkdirp.sync(themesDir);
|
||||
|
||||
try {
|
||||
files.map(async file => {
|
||||
@@ -32,7 +34,7 @@ try {
|
||||
|
||||
const formattedStyles = prettier.format(stripComments(css), { parser: 'css' });
|
||||
const filename = path.basename(file).replace('.styles.ts', '.css');
|
||||
const outfile = path.join(outdir, filename);
|
||||
const outfile = path.join(themesDir, filename);
|
||||
await fs.writeFile(outfile, formattedStyles, 'utf8');
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//
|
||||
import Promise from 'bluebird';
|
||||
import chalk from 'chalk';
|
||||
import commandLineArgs from 'command-line-args';
|
||||
import copy from 'recursive-copy';
|
||||
import del from 'del';
|
||||
import download from 'download';
|
||||
@@ -13,7 +14,9 @@ import { stat, readFile, writeFile } from 'fs/promises';
|
||||
import glob from 'globby';
|
||||
import path from 'path';
|
||||
|
||||
const iconDir = './dist/assets/icons';
|
||||
const { outdir } = commandLineArgs({ name: 'outdir', type: String });
|
||||
const iconDir = path.join(outdir, '/assets/icons');
|
||||
|
||||
const iconPackageData = JSON.parse(readFileSync('./node_modules/bootstrap-icons/package.json', 'utf8'));
|
||||
let numIcons = 0;
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
// This script runs the Custom Elements Manifest analyzer to generate custom-elements.json
|
||||
//
|
||||
import chalk from 'chalk';
|
||||
import mkdirp from 'mkdirp';
|
||||
import commandLineArgs from 'command-line-args';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
mkdirp.sync('./dist');
|
||||
const { outdir } = commandLineArgs({ name: 'outdir', type: String });
|
||||
|
||||
// Run the analyzer
|
||||
console.log('Generating component metadata');
|
||||
execSync('cem analyze --litelement --outdir dist', { stdio: 'inherit' });
|
||||
execSync(`cem analyze --litelement --outdir "${outdir}"`, { stdio: 'inherit' });
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import commandLineArgs from 'command-line-args';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import glob from 'globby';
|
||||
import lunr from 'lunr';
|
||||
import { getAllComponents } from './shared.js';
|
||||
|
||||
const metadata = JSON.parse(fs.readFileSync('./dist/custom-elements.json', 'utf8'));
|
||||
const { outdir } = commandLineArgs({ name: 'outdir', type: String });
|
||||
const metadata = JSON.parse(fs.readFileSync(path.join(outdir, 'custom-elements.json'), 'utf8'));
|
||||
|
||||
console.log('Generating search index for documentation');
|
||||
|
||||
|
||||
@@ -4,10 +4,13 @@
|
||||
// You must generate dist/custom-elements.json before running this script.
|
||||
//
|
||||
import chalk from 'chalk';
|
||||
import commandLineArgs from 'command-line-args';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { getAllComponents } from './shared.js';
|
||||
|
||||
const metadata = JSON.parse(fs.readFileSync('./dist/custom-elements.json', 'utf8'));
|
||||
const { outdir } = commandLineArgs({ name: 'outdir', type: String });
|
||||
const metadata = JSON.parse(fs.readFileSync(path.join(outdir, 'custom-elements.json'), 'utf8'));
|
||||
|
||||
console.log('Generating custom data for VS Code');
|
||||
|
||||
@@ -53,4 +56,4 @@ components.map(component => {
|
||||
vscode.tags.push({ name, attributes });
|
||||
});
|
||||
|
||||
fs.writeFileSync('./dist/vscode.html-custom-data.json', JSON.stringify(vscode, null, 2), 'utf8');
|
||||
fs.writeFileSync(path.join(outdir, 'vscode.html-custom-data.json'), JSON.stringify(vscode, null, 2), 'utf8');
|
||||
|
||||
@@ -15,9 +15,9 @@ export default css`
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
background-color: rgb(var(--sl-color-neutral-0));
|
||||
border: solid 1px rgb(var(--sl-color-neutral-200));
|
||||
border-top-width: 3px;
|
||||
background-color: rgb(var(--sl-surface-base-alt));
|
||||
border: solid var(--sl-panel-border-width) rgb(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);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
@@ -22,9 +22,9 @@ const toastStack = Object.assign(document.createElement('div'), { className: 'sl
|
||||
* @slot icon - An icon to show in the alert.
|
||||
*
|
||||
* @event sl-show - Emitted when the alert opens.
|
||||
* @event sl-after-show - Emitted after the alert opens and all transitions are complete.
|
||||
* @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 transitions are complete.
|
||||
* @event sl-after-hide - Emitted after the alert closes and all animations are complete.
|
||||
*
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart icon - The container that wraps the alert icon.
|
||||
|
||||
52
src/components/animated-image/animated-image.styles.ts
Normal file
52
src/components/animated-image/animated-image.styles.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
--control-box-size: 2.5rem;
|
||||
--icon-size: calc(var(--control-box-size) * 0.625);
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
img[aria-hidden='true'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.animated-image__control-box {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
top: calc(50% - var(--control-box-size) / 2);
|
||||
right: calc(50% - var(--control-box-size) / 2);
|
||||
width: var(--control-box-size);
|
||||
height: var(--control-box-size);
|
||||
font-size: var(--icon-size);
|
||||
background: none;
|
||||
border: none;
|
||||
background-color: rgb(var(--sl-color-neutral-1000) / 50%);
|
||||
border-radius: var(--sl-border-radius-circle);
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
pointer-events: none;
|
||||
transition: var(--sl-transition-fast) opacity;
|
||||
}
|
||||
|
||||
:host([play]:hover) .animated-image__control-box {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
:host([play]:not(:hover)) .animated-image__control-box {
|
||||
opacity: 0;
|
||||
}
|
||||
`;
|
||||
13
src/components/animated-image/animated-image.test.ts
Normal file
13
src/components/animated-image/animated-image.test.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
// import sinon from 'sinon';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlAnimatedImage from './animated-image';
|
||||
|
||||
describe('<sl-animated-image>', () => {
|
||||
it('should render a component', async () => {
|
||||
const el = await fixture(html` <sl-animated-image></sl-animated-image> `);
|
||||
|
||||
expect(el).to.exist;
|
||||
});
|
||||
});
|
||||
120
src/components/animated-image/animated-image.ts
Normal file
120
src/components/animated-image/animated-image.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { emit } from '../../internal/event';
|
||||
import styles from './animated-image.styles';
|
||||
|
||||
import '../icon/icon';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
* @status experimental
|
||||
*
|
||||
* @dependency sl-icon
|
||||
*
|
||||
* @event sl-load - Emitted when the image loads successfully.
|
||||
* @event sl-error - Emitted when the image fails to load.
|
||||
*
|
||||
* @part - control-box - The container that surrounds the pause/play icons and provides their background.
|
||||
* @part - play-icon - The icon to use for the play button.
|
||||
* @part - pause-icon - The icon to use for the pause button.
|
||||
*
|
||||
* @cssproperty --control-box-size - The size of the icon box.
|
||||
* @cssproperty --icon-size - The size of the play/pause icons.
|
||||
*/
|
||||
@customElement('sl-animated-image')
|
||||
export default class SlAnimatedImage extends LitElement {
|
||||
static styles = styles;
|
||||
|
||||
@state() frozenFrame: string;
|
||||
@state() isLoaded = false;
|
||||
|
||||
@query('.animated-image__animated') animatedImage: HTMLImageElement;
|
||||
|
||||
/** The image's src attribute. */
|
||||
@property() src: string;
|
||||
|
||||
/** The image's alt attribute. */
|
||||
@property() alt: string;
|
||||
|
||||
/** When set, the image will animate. Otherwise, it will be paused. */
|
||||
@property({ type: Boolean, reflect: true }) play: boolean;
|
||||
|
||||
handleClick() {
|
||||
this.play = !this.play;
|
||||
}
|
||||
|
||||
handleLoad() {
|
||||
const canvas = document.createElement('canvas');
|
||||
const { width, height } = this.animatedImage;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
canvas.getContext('2d')!.drawImage(this.animatedImage, 0, 0, width, height);
|
||||
this.frozenFrame = canvas.toDataURL('image/gif');
|
||||
|
||||
if (!this.isLoaded) {
|
||||
emit(this, 'sl-load');
|
||||
this.isLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
handleError() {
|
||||
emit(this, 'sl-error');
|
||||
}
|
||||
|
||||
@watch('play')
|
||||
async handlePlayChange() {
|
||||
// When the animation starts playing, reset the src so it plays from the beginning. Since the src is cached, this
|
||||
// won't trigger another request.
|
||||
if (this.play) {
|
||||
this.animatedImage.src = '';
|
||||
this.animatedImage.src = this.src;
|
||||
}
|
||||
}
|
||||
|
||||
@watch('src')
|
||||
handleSrcChange() {
|
||||
this.isLoaded = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="animated-image">
|
||||
<img
|
||||
class="animated-image__animated"
|
||||
src=${this.src}
|
||||
alt=${this.alt}
|
||||
crossorigin="anonymous"
|
||||
aria-hidden=${this.play ? 'false' : 'true'}
|
||||
@click=${this.handleClick}
|
||||
@load=${this.handleLoad}
|
||||
@error=${this.handleError}
|
||||
/>
|
||||
|
||||
${this.isLoaded
|
||||
? html`
|
||||
<img
|
||||
class="animated-image__frozen"
|
||||
src=${this.frozenFrame}
|
||||
alt=${this.alt}
|
||||
aria-hidden=${this.play ? 'true' : 'false'}
|
||||
@click=${this.handleClick}
|
||||
/>
|
||||
|
||||
<div part="control-box" class="animated-image__control-box">
|
||||
${this.play
|
||||
? html`<sl-icon part="pause-icon" name="pause-fill" library="system"></sl-icon>`
|
||||
: html`<sl-icon part="play-icon" name="play-fill" library="system"></sl-icon>`}
|
||||
</div>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-animated-image': SlAnimatedImage;
|
||||
}
|
||||
}
|
||||
126
src/components/avatar/avatar.test.ts
Normal file
126
src/components/avatar/avatar.test.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlAvatar from './avatar';
|
||||
|
||||
describe('<sl-avatar>', () => {
|
||||
let el: SlAvatar;
|
||||
|
||||
describe('when provided no parameters', async () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlAvatar>(html` <sl-avatar></sl-avatar> `);
|
||||
});
|
||||
|
||||
it('passes accessibility test', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should default to circle styling', async () => {
|
||||
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
expect(el.getAttribute('shape')).to.eq('circle');
|
||||
expect(part.classList.value.trim()).to.eq('avatar avatar--circle');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided an image and alt parameter', async () => {
|
||||
const image =
|
||||
'https://images.unsplash.com/photo-1529778873920-4da4926a72c2?ixlib=rb-1.2.1&auto=format&fit=crop&w=300&q=80';
|
||||
const alt = 'Gray tabby kitten looking down';
|
||||
before(async () => {
|
||||
el = await fixture<SlAvatar>(html`<sl-avatar image="${image}" alt="${alt}"></sl-avatar>`);
|
||||
});
|
||||
|
||||
it('passes accessibility test', async () => {
|
||||
/**
|
||||
* The image element itself is ancillary, because it's parent container contains the
|
||||
* aria-label which dictates what "sl-avatar" is. This also implies that alt text will
|
||||
* resolve to "" when not provided and ignored by readers. This is why we use alt="" on
|
||||
* the image element to pass accessibility.
|
||||
* https://html.spec.whatwg.org/multipage/images.html#ancillary-images
|
||||
*/
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('renders "image" part, with src and a role of presentation', async () => {
|
||||
const part = el.shadowRoot?.querySelector('[part="image"]') as HTMLImageElement;
|
||||
|
||||
expect(part.getAttribute('src')).to.eq(image);
|
||||
});
|
||||
|
||||
it('renders the alt attribute in the "base" part', async () => {
|
||||
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
|
||||
expect(part.getAttribute('aria-label')).to.eq(alt);
|
||||
});
|
||||
|
||||
describe('when an error occurs when attempting to load the image', async () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlAvatar>(
|
||||
html`<sl-avatar image="data:text/plain;not-an-image-url" alt="${alt}"></sl-avatar>`
|
||||
);
|
||||
});
|
||||
|
||||
it('does not render the "image" part', async () => {
|
||||
const part = el.shadowRoot?.querySelector('[part="image"]') as HTMLImageElement;
|
||||
|
||||
expect(part).not.to.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided initials parameter', async () => {
|
||||
const initials = 'SL';
|
||||
before(async () => {
|
||||
el = await fixture<SlAvatar>(html`<sl-avatar initials="${initials}"></sl-avatar>`);
|
||||
});
|
||||
|
||||
it('passes accessibility test', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('renders "initials" part, with initials as the text node', async () => {
|
||||
const part = el.shadowRoot?.querySelector('[part="initials"]') as HTMLImageElement;
|
||||
|
||||
expect(part.innerText).to.eq(initials);
|
||||
});
|
||||
});
|
||||
|
||||
['square', 'rounded', 'circle'].forEach(shape => {
|
||||
describe(`when passed a shape attribute ${shape}`, () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlAvatar>(html`<sl-avatar shape="${shape}"></sl-avatar>`);
|
||||
});
|
||||
|
||||
it('passes accessibility test', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('appends the appropriate class on the "base" part', async () => {
|
||||
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
|
||||
expect(el.getAttribute('shape')).to.eq(shape);
|
||||
expect(part.classList.value.trim()).to.eq(`avatar avatar--${shape}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when passed a <span>, on slot "icon"', async () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlAvatar>(html`<sl-avatar><span slot="icon">random content</span></sl-avatar>`);
|
||||
});
|
||||
|
||||
it('passes accessibility test', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should accept as an assigned child in the shadow root', async () => {
|
||||
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=icon]');
|
||||
const childNodes = slot.assignedNodes({ flatten: true });
|
||||
|
||||
expect(childNodes.length).to.eq(1);
|
||||
|
||||
const span = <HTMLElement>childNodes[0];
|
||||
expect(span.innerHTML).to.eq('random content');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import styles from './avatar.styles';
|
||||
|
||||
import '../icon/icon';
|
||||
@@ -61,7 +61,13 @@ export default class SlAvatar extends LitElement {
|
||||
`}
|
||||
${this.image && !this.hasError
|
||||
? html`
|
||||
<img part="image" class="avatar__image" src="${this.image}" @error="${() => (this.hasError = true)}" />
|
||||
<img
|
||||
part="image"
|
||||
class="avatar__image"
|
||||
src="${this.image}"
|
||||
alt=""
|
||||
@error="${() => (this.hasError = true)}"
|
||||
/>
|
||||
`
|
||||
: ''}
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,7 @@ export default css`
|
||||
letter-spacing: var(--sl-letter-spacing-normal);
|
||||
line-height: 1;
|
||||
border-radius: var(--sl-border-radius-small);
|
||||
border: solid 1px rgb(var(--sl-color-neutral-0));
|
||||
white-space: nowrap;
|
||||
padding: 3px 6px;
|
||||
user-select: none;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import styles from './badge.styles';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,13 +1,160 @@
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
// import sinon from 'sinon';
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlBreadcrumbItem from './breadcrumb-item';
|
||||
|
||||
describe('<sl-breadcrumb-item>', () => {
|
||||
it('should render a component', async () => {
|
||||
const el = await fixture(html` <sl-breadcrumb-item></sl-breadcrumb-item> `);
|
||||
let el: SlBreadcrumbItem;
|
||||
|
||||
expect(el).to.exist;
|
||||
describe('when not provided a href attribute', async () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlBreadcrumbItem>(html` <sl-breadcrumb-item>Home</sl-breadcrumb-item> `);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should hide the seperator from screen readers', async () => {
|
||||
const separator: HTMLSpanElement = el.shadowRoot.querySelector('[part="separator"]');
|
||||
expect(separator).attribute('aria-hidden', 'true');
|
||||
});
|
||||
|
||||
it('should render a HTMLButtonElement as the part "label", with a set type "button"', () => {
|
||||
const button: HTMLButtonElement = el.shadowRoot.querySelector('[part="label"]');
|
||||
expect(button).to.exist;
|
||||
expect(button).attribute('type', 'button');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a href attribute', async () => {
|
||||
describe('and no target', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlBreadcrumbItem>(html`
|
||||
<sl-breadcrumb-item href="https://jsonplaceholder.typicode.com/">Home</sl-breadcrumb-item>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should render a HTMLAnchorElement as the part "label", with the supplied href value', () => {
|
||||
const hyperlink: HTMLAnchorElement = el.shadowRoot.querySelector('[part="label"]');
|
||||
expect(hyperlink).attribute('href', 'https://jsonplaceholder.typicode.com/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('and target, without rel', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlBreadcrumbItem>(html`
|
||||
<sl-breadcrumb-item href="https://jsonplaceholder.typicode.com/" target="_blank">Help</sl-breadcrumb-item>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
describe('should render a HTMLAnchorElement as the part "label"', () => {
|
||||
let hyperlink: HTMLAnchorElement;
|
||||
|
||||
before(() => {
|
||||
hyperlink = el.shadowRoot.querySelector('[part="label"]');
|
||||
});
|
||||
|
||||
it('should use the supplied href value, as the href attribute value', () => {
|
||||
expect(hyperlink).attribute('href', 'https://jsonplaceholder.typicode.com/');
|
||||
});
|
||||
|
||||
it('should default rel attribute to "noreferrer noopener"', () => {
|
||||
expect(hyperlink).attribute('rel', 'noreferrer noopener');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and target, with rel', () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlBreadcrumbItem>(html`
|
||||
<sl-breadcrumb-item href="https://jsonplaceholder.typicode.com/" target="_blank" rel="alternate"
|
||||
>Help</sl-breadcrumb-item
|
||||
>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
describe('should render a HTMLAnchorElement', () => {
|
||||
let hyperlink: HTMLAnchorElement;
|
||||
|
||||
before(() => {
|
||||
hyperlink = el.shadowRoot.querySelector('a');
|
||||
});
|
||||
|
||||
it('should use the supplied href value, as the href attribute value', () => {
|
||||
expect(hyperlink).attribute('href', 'https://jsonplaceholder.typicode.com/');
|
||||
});
|
||||
|
||||
it('should use the supplied rel value, as the rel attribute value', () => {
|
||||
expect(hyperlink).attribute('rel', 'alternate');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided an element in the slot "prefix" to support prefix icons', async () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlBreadcrumbItem>(html`
|
||||
<sl-breadcrumb-item>
|
||||
<span class="prefix-example" slot="prefix">/</span>
|
||||
Home
|
||||
</sl-breadcrumb-item>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should accept as an assigned child in the shadow root', () => {
|
||||
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=prefix]');
|
||||
const childNodes = slot.assignedNodes({ flatten: true });
|
||||
|
||||
expect(childNodes.length).to.eq(1);
|
||||
});
|
||||
|
||||
it('should append class "breadcrumb-item--has-prefix" to "base" part', () => {
|
||||
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
expect(part.classList.value).to.equal('breadcrumb-item breadcrumb-item--has-prefix');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided an element in the slot "suffix" to support suffix icons', async () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlBreadcrumbItem>(html`
|
||||
<sl-breadcrumb-item>
|
||||
<span class="prefix-example" slot="suffix">/</span>
|
||||
Security
|
||||
</sl-breadcrumb-item>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should accept as an assigned child in the shadow root', () => {
|
||||
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=suffix]');
|
||||
const childNodes = slot.assignedNodes({ flatten: true });
|
||||
|
||||
expect(childNodes.length).to.eq(1);
|
||||
});
|
||||
|
||||
it('should append class "breadcrumb-item--has-suffix" to "base" part', () => {
|
||||
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
|
||||
expect(part.classList.value).to.equal('breadcrumb-item breadcrumb-item--has-suffix');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map.js';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { hasSlot } from '../../internal/slot';
|
||||
import styles from './breadcrumb-item.styles';
|
||||
|
||||
@@ -28,12 +28,18 @@ export default class SlBreadcrumbItem extends LitElement {
|
||||
@state() hasPrefix = false;
|
||||
@state() hasSuffix = false;
|
||||
|
||||
/** Optional link to direct the user to when the breadcrumb item is activated. */
|
||||
/**
|
||||
* Optional URL to direct the user to when the breadcrumb item is activated. When set, a link will be rendered
|
||||
* internally. When unset, a button will be rendered instead.
|
||||
*/
|
||||
@property() href: string;
|
||||
|
||||
/** Tells the browser where to open the link. Only used when `href` is set. */
|
||||
@property() target: '_blank' | '_parent' | '_self' | '_top';
|
||||
|
||||
/** The `rel` attribute to use on the link. Only used when `href` is set. */
|
||||
@property() rel: string = 'noreferrer noopener';
|
||||
|
||||
handleSlotChange() {
|
||||
this.hasPrefix = hasSlot(this, 'prefix');
|
||||
this.hasSuffix = hasSlot(this, 'suffix');
|
||||
@@ -62,7 +68,7 @@ export default class SlBreadcrumbItem extends LitElement {
|
||||
class="breadcrumb-item__label breadcrumb-item__label--link"
|
||||
href="${this.href}"
|
||||
target="${this.target}"
|
||||
rel=${ifDefined(this.target ? 'noreferrer noopener' : undefined)}
|
||||
rel=${ifDefined(this.target ? this.rel : undefined)}
|
||||
>
|
||||
<slot></slot>
|
||||
</a>
|
||||
|
||||
@@ -1,13 +1,104 @@
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
// import sinon from 'sinon';
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlBreadcrumb from './breadcrumb';
|
||||
|
||||
describe('<sl-breadcrumb>', () => {
|
||||
it('should render a component', async () => {
|
||||
const el = await fixture(html` <sl-breadcrumb></sl-breadcrumb> `);
|
||||
let el: SlBreadcrumb;
|
||||
|
||||
expect(el).to.exist;
|
||||
describe('when provided a standard list of el-breadcrumb-item children and no parameters', async () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlBreadcrumb>(html`
|
||||
<sl-breadcrumb>
|
||||
<sl-breadcrumb-item>Catalog</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>Clothing</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>Women's</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>Shirts & Tops</sl-breadcrumb-item>
|
||||
</sl-breadcrumb>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should render sl-icon as separator', async () => {
|
||||
expect(el.querySelectorAll('sl-icon').length).to.eq(4);
|
||||
});
|
||||
|
||||
it('should attach aria-current "page" on the last breadcrumb item.', async () => {
|
||||
const breadcrumbItems = el.querySelectorAll('sl-breadcrumb-item');
|
||||
const lastNode = breadcrumbItems[3];
|
||||
expect(lastNode).attribute('aria-current', 'page');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "seperator" to support Custom Separators', async () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlBreadcrumb>(html`
|
||||
<sl-breadcrumb>
|
||||
<span class="replacement-separator" slot="separator">/</span>
|
||||
<sl-breadcrumb-item>First</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>Second</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>Third</sl-breadcrumb-item>
|
||||
</sl-breadcrumb>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should accept "separator" as an assigned child in the shadow root', async () => {
|
||||
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=separator]');
|
||||
const childNodes = slot.assignedNodes({ flatten: true });
|
||||
|
||||
expect(childNodes.length).to.eq(1);
|
||||
});
|
||||
|
||||
it('should replace the sl-icon separator with the provided separator', async () => {
|
||||
expect(el.querySelectorAll('.replacement-separator').length).to.eq(4);
|
||||
expect(el.querySelectorAll('sl-icon').length).to.eq(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "prefix" to support prefix icons', async () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlBreadcrumb>(html`
|
||||
<sl-breadcrumb>
|
||||
<sl-breadcrumb-item>
|
||||
<span class="prefix-example" slot="prefix">/</span>
|
||||
Home
|
||||
</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>First</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>Second</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>Third</sl-breadcrumb-item>
|
||||
</sl-breadcrumb>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "suffix" to support suffix icons', async () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlBreadcrumb>(html`
|
||||
<sl-breadcrumb>
|
||||
<sl-breadcrumb-item>First</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>Second</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>Third</sl-breadcrumb-item>
|
||||
<sl-breadcrumb-item>
|
||||
<span class="prefix-example" slot="suffix">/</span>
|
||||
Security
|
||||
</sl-breadcrumb-item>
|
||||
</sl-breadcrumb>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,13 +38,13 @@ export default css`
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.button.button--disabled {
|
||||
.button--disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* When disabled, prevent mouse events from bubbling up */
|
||||
.button.button--disabled * {
|
||||
.button--disabled * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -69,160 +69,302 @@ export default css`
|
||||
*/
|
||||
|
||||
/* Default */
|
||||
.button.button--default {
|
||||
.button--standard.button--default {
|
||||
background-color: rgb(var(--sl-color-neutral-0));
|
||||
border-color: rgb(var(--sl-color-neutral-300));
|
||||
color: rgb(var(--sl-color-neutral-700));
|
||||
}
|
||||
|
||||
.button.button--default:hover:not(.button--disabled) {
|
||||
.button--standard.button--default:hover:not(.button--disabled) {
|
||||
background-color: rgb(var(--sl-color-primary-50));
|
||||
border-color: rgb(var(--sl-color-primary-300));
|
||||
color: rgb(var(--sl-color-primary-700));
|
||||
}
|
||||
|
||||
.button.button--default${focusVisibleSelector}:not(.button--disabled) {
|
||||
.button--standard.button--default${focusVisibleSelector}:not(.button--disabled) {
|
||||
background-color: rgb(var(--sl-color-primary-50));
|
||||
border-color: rgb(var(--sl-color-primary-400));
|
||||
color: rgb(var(--sl-color-primary-700));
|
||||
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-primary-500) / var(--sl-focus-ring-alpha));
|
||||
}
|
||||
|
||||
.button.button--default:active:not(.button--disabled) {
|
||||
.button--standard.button--default:active:not(.button--disabled) {
|
||||
background-color: rgb(var(--sl-color-primary-100));
|
||||
border-color: rgb(var(--sl-color-primary-400));
|
||||
color: rgb(var(--sl-color-primary-700));
|
||||
}
|
||||
|
||||
/* Primary */
|
||||
.button.button--primary {
|
||||
.button--standard.button--primary {
|
||||
background-color: rgb(var(--sl-color-primary-600));
|
||||
border-color: rgb(var(--sl-color-primary-600));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
|
||||
.button.button--primary:hover:not(.button--disabled) {
|
||||
.button--standard.button--primary:hover:not(.button--disabled) {
|
||||
background-color: rgb(var(--sl-color-primary-500));
|
||||
border-color: rgb(var(--sl-color-primary-500));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
|
||||
.button.button--primary${focusVisibleSelector}:not(.button--disabled) {
|
||||
.button--standard.button--primary${focusVisibleSelector}:not(.button--disabled) {
|
||||
background-color: rgb(var(--sl-color-primary-500));
|
||||
border-color: rgb(var(--sl-color-primary-500));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-primary-500) / var(--sl-focus-ring-alpha));
|
||||
}
|
||||
|
||||
.button.button--primary:active:not(.button--disabled) {
|
||||
.button--standard.button--primary:active:not(.button--disabled) {
|
||||
background-color: rgb(var(--sl-color-primary-600));
|
||||
border-color: rgb(var(--sl-color-primary-600));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
|
||||
/* Success */
|
||||
.button.button--success {
|
||||
.button--standard.button--success {
|
||||
background-color: rgb(var(--sl-color-success-600));
|
||||
border-color: rgb(var(--sl-color-success-600));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
|
||||
.button.button--success:hover:not(.button--disabled) {
|
||||
.button--standard.button--success:hover:not(.button--disabled) {
|
||||
background-color: rgb(var(--sl-color-success-500));
|
||||
border-color: rgb(var(--sl-color-success-500));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
|
||||
.button.button--success${focusVisibleSelector}:not(.button--disabled) {
|
||||
.button--standard.button--success${focusVisibleSelector}:not(.button--disabled) {
|
||||
background-color: rgb(var(--sl-color-success-600));
|
||||
border-color: rgb(var(--sl-color-success-600));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-success-500) / var(--sl-focus-ring-alpha));
|
||||
}
|
||||
|
||||
.button.button--success:active:not(.button--disabled) {
|
||||
.button--standard.button--success:active:not(.button--disabled) {
|
||||
background-color: rgb(var(--sl-color-success-600));
|
||||
border-color: rgb(var(--sl-color-success-600));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
|
||||
/* Neutral */
|
||||
.button.button--neutral {
|
||||
.button--standard.button--neutral {
|
||||
background-color: rgb(var(--sl-color-neutral-600));
|
||||
border-color: rgb(var(--sl-color-neutral-600));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
|
||||
.button.button--neutral:hover:not(.button--disabled) {
|
||||
.button--standard.button--neutral:hover:not(.button--disabled) {
|
||||
background-color: rgb(var(--sl-color-neutral-500));
|
||||
border-color: rgb(var(--sl-color-neutral-500));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
|
||||
.button.button--neutral${focusVisibleSelector}:not(.button--disabled) {
|
||||
.button--standard.button--neutral${focusVisibleSelector}:not(.button--disabled) {
|
||||
background-color: rgb(var(--sl-color-neutral-500));
|
||||
border-color: rgb(var(--sl-color-neutral-500));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-neutral-500) / var(--sl-focus-ring-alpha));
|
||||
}
|
||||
|
||||
.button.button--neutral:active:not(.button--disabled) {
|
||||
.button--standard.button--neutral:active:not(.button--disabled) {
|
||||
background-color: rgb(var(--sl-color-neutral-600));
|
||||
border-color: rgb(var(--sl-color-neutral-600));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
|
||||
/* Warning */
|
||||
.button.button--warning {
|
||||
.button--standard.button--warning {
|
||||
background-color: rgb(var(--sl-color-warning-600));
|
||||
border-color: rgb(var(--sl-color-warning-600));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
.button.button--warning:hover:not(.button--disabled) {
|
||||
.button--standard.button--warning:hover:not(.button--disabled) {
|
||||
background-color: rgb(var(--sl-color-warning-500));
|
||||
border-color: rgb(var(--sl-color-warning-500));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
|
||||
.button.button--warning${focusVisibleSelector}:not(.button--disabled) {
|
||||
.button--standard.button--warning${focusVisibleSelector}:not(.button--disabled) {
|
||||
background-color: rgb(var(--sl-color-warning-500));
|
||||
border-color: rgb(var(--sl-color-warning-500));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-warning-500) / var(--sl-focus-ring-alpha));
|
||||
}
|
||||
|
||||
.button.button--warning:active:not(.button--disabled) {
|
||||
.button--standard.button--warning:active:not(.button--disabled) {
|
||||
background-color: rgb(var(--sl-color-warning-600));
|
||||
border-color: rgb(var(--sl-color-warning-600));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
|
||||
/* Danger */
|
||||
.button.button--danger {
|
||||
.button--standard.button--danger {
|
||||
background-color: rgb(var(--sl-color-danger-600));
|
||||
border-color: rgb(var(--sl-color-danger-600));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
|
||||
.button.button--danger:hover:not(.button--disabled) {
|
||||
.button--standard.button--danger:hover:not(.button--disabled) {
|
||||
background-color: rgb(var(--sl-color-danger-500));
|
||||
border-color: rgb(var(--sl-color-danger-500));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
|
||||
.button.button--danger${focusVisibleSelector}:not(.button--disabled) {
|
||||
.button--standard.button--danger${focusVisibleSelector}:not(.button--disabled) {
|
||||
background-color: rgb(var(--sl-color-danger-500));
|
||||
border-color: rgb(var(--sl-color-danger-500));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-danger-500) / var(--sl-focus-ring-alpha));
|
||||
}
|
||||
|
||||
.button.button--danger:active:not(.button--disabled) {
|
||||
.button--standard.button--danger:active:not(.button--disabled) {
|
||||
background-color: rgb(var(--sl-color-danger-600));
|
||||
border-color: rgb(var(--sl-color-danger-600));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
|
||||
/*
|
||||
* Outline buttons
|
||||
*/
|
||||
|
||||
.button--outline {
|
||||
background: none;
|
||||
border: solid 1px;
|
||||
}
|
||||
|
||||
/* Default */
|
||||
.button--outline.button--default {
|
||||
border-color: rgb(var(--sl-color-neutral-300));
|
||||
color: rgb(var(--sl-color-neutral-700));
|
||||
}
|
||||
|
||||
.button--outline.button--default:hover:not(.button--disabled) {
|
||||
border-color: rgb(var(--sl-color-primary-600));
|
||||
background-color: rgb(var(--sl-color-primary-600));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
|
||||
.button--outline.button--default${focusVisibleSelector}:not(.button--disabled) {
|
||||
border-color: rgb(var(--sl-color-primary-500));
|
||||
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-primary-500) / var(--sl-focus-ring-alpha));
|
||||
}
|
||||
|
||||
.button--outline.button--default:active:not(.button--disabled) {
|
||||
border-color: rgb(var(--sl-color-primary-700));
|
||||
background-color: rgb(var(--sl-color-primary-700));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
|
||||
/* Primary */
|
||||
.button--outline.button--primary {
|
||||
border-color: rgb(var(--sl-color-primary-600));
|
||||
color: rgb(var(--sl-color-primary-600));
|
||||
}
|
||||
|
||||
.button--outline.button--primary:hover:not(.button--disabled) {
|
||||
background-color: rgb(var(--sl-color-primary-600));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
|
||||
.button--outline.button--primary${focusVisibleSelector}:not(.button--disabled) {
|
||||
border-color: rgb(var(--sl-color-primary-500));
|
||||
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-primary-500) / var(--sl-focus-ring-alpha));
|
||||
}
|
||||
|
||||
.button--outline.button--primary:active:not(.button--disabled) {
|
||||
border-color: rgb(var(--sl-color-primary-700));
|
||||
background-color: rgb(var(--sl-color-primary-700));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
|
||||
/* Success */
|
||||
.button--outline.button--success {
|
||||
border-color: rgb(var(--sl-color-success-600));
|
||||
color: rgb(var(--sl-color-success-600));
|
||||
}
|
||||
|
||||
.button--outline.button--success:hover:not(.button--disabled) {
|
||||
background-color: rgb(var(--sl-color-success-600));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
|
||||
.button--outline.button--success${focusVisibleSelector}:not(.button--disabled) {
|
||||
border-color: rgb(var(--sl-color-success-500));
|
||||
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-success-500) / var(--sl-focus-ring-alpha));
|
||||
}
|
||||
|
||||
.button--outline.button--success:active:not(.button--disabled) {
|
||||
border-color: rgb(var(--sl-color-success-700));
|
||||
background-color: rgb(var(--sl-color-success-700));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
|
||||
/* Neutral */
|
||||
.button--outline.button--neutral {
|
||||
border-color: rgb(var(--sl-color-neutral-600));
|
||||
color: rgb(var(--sl-color-neutral-600));
|
||||
}
|
||||
|
||||
.button--outline.button--neutral:hover:not(.button--disabled) {
|
||||
background-color: rgb(var(--sl-color-neutral-600));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
|
||||
.button--outline.button--neutral${focusVisibleSelector}:not(.button--disabled) {
|
||||
border-color: rgb(var(--sl-color-neutral-500));
|
||||
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-neutral-500) / var(--sl-focus-ring-alpha));
|
||||
}
|
||||
|
||||
.button--outline.button--neutral:active:not(.button--disabled) {
|
||||
border-color: rgb(var(--sl-color-neutral-700));
|
||||
background-color: rgb(var(--sl-color-neutral-700));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
|
||||
/* Warning */
|
||||
.button--outline.button--warning {
|
||||
border-color: rgb(var(--sl-color-warning-600));
|
||||
color: rgb(var(--sl-color-warning-600));
|
||||
}
|
||||
|
||||
.button--outline.button--warning:hover:not(.button--disabled) {
|
||||
background-color: rgb(var(--sl-color-warning-600));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
|
||||
.button--outline.button--warning${focusVisibleSelector}:not(.button--disabled) {
|
||||
border-color: rgb(var(--sl-color-warning-500));
|
||||
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-warning-500) / var(--sl-focus-ring-alpha));
|
||||
}
|
||||
|
||||
.button--outline.button--warning:active:not(.button--disabled) {
|
||||
border-color: rgb(var(--sl-color-warning-700));
|
||||
background-color: rgb(var(--sl-color-warning-700));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
|
||||
/* Danger */
|
||||
.button--outline.button--danger {
|
||||
border-color: rgb(var(--sl-color-danger-600));
|
||||
color: rgb(var(--sl-color-danger-600));
|
||||
}
|
||||
|
||||
.button--outline.button--danger:hover:not(.button--disabled) {
|
||||
background-color: rgb(var(--sl-color-danger-600));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
|
||||
.button--outline.button--danger${focusVisibleSelector}:not(.button--disabled) {
|
||||
border-color: rgb(var(--sl-color-danger-500));
|
||||
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-danger-500) / var(--sl-focus-ring-alpha));
|
||||
}
|
||||
|
||||
.button--outline.button--danger:active:not(.button--disabled) {
|
||||
border-color: rgb(var(--sl-color-danger-700));
|
||||
background-color: rgb(var(--sl-color-danger-700));
|
||||
color: rgb(var(--sl-color-neutral-0));
|
||||
}
|
||||
|
||||
/*
|
||||
* Text buttons
|
||||
*/
|
||||
@@ -456,7 +598,7 @@ export default css`
|
||||
* buttons and we style them here instead.
|
||||
*/
|
||||
|
||||
:host(.sl-button-group__button--first) .button {
|
||||
:host(.sl-button-group__button--first:not(.sl-button-group__button--last)) .button {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
@@ -465,7 +607,7 @@ export default css`
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
:host(.sl-button-group__button--last) .button {
|
||||
:host(.sl-button-group__button--last:not(.sl-button-group__button--first)) .button {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map.js';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { emit } from '../../internal/event';
|
||||
import { hasSlot } from '../../internal/slot';
|
||||
import styles from './button.styles';
|
||||
@@ -54,6 +54,9 @@ export default class SlButton extends LitElement {
|
||||
/** Draws the button in a loading state. */
|
||||
@property({ type: Boolean, reflect: true }) loading = false;
|
||||
|
||||
/** Draws an outlined button. */
|
||||
@property({ type: Boolean, reflect: true }) outline = false;
|
||||
|
||||
/** Draws a pill-style button with rounded edges. */
|
||||
@property({ type: Boolean, reflect: true }) pill = false;
|
||||
|
||||
@@ -174,6 +177,8 @@ export default class SlButton extends LitElement {
|
||||
'button--disabled': this.disabled,
|
||||
'button--focused': this.hasFocus,
|
||||
'button--loading': this.loading,
|
||||
'button--standard': !this.outline,
|
||||
'button--outline': this.outline,
|
||||
'button--pill': this.pill,
|
||||
'button--has-label': this.hasLabel,
|
||||
'button--has-prefix': this.hasPrefix,
|
||||
@@ -213,6 +218,8 @@ export default class SlButton extends LitElement {
|
||||
'button--disabled': this.disabled,
|
||||
'button--focused': this.hasFocus,
|
||||
'button--loading': this.loading,
|
||||
'button--standard': !this.outline,
|
||||
'button--outline': this.outline,
|
||||
'button--pill': this.pill,
|
||||
'button--has-label': this.hasLabel,
|
||||
'button--has-prefix': this.hasPrefix,
|
||||
|
||||
@@ -16,7 +16,7 @@ export default css`
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: rgb(var(--sl-color-neutral-0));
|
||||
background-color: rgb(var(--sl-surface-base-alt));
|
||||
box-shadow: var(--sl-shadow-x-small);
|
||||
border: solid var(--border-width) var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
139
src/components/card/card.test.ts
Normal file
139
src/components/card/card.test.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlCard from './card';
|
||||
|
||||
describe('<sl-card>', () => {
|
||||
let el: SlCard;
|
||||
|
||||
describe('when provided no parameters', async () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlCard>(
|
||||
html` <sl-card>This is just a basic card. No image, no header, and no footer. Just your content.</sl-card> `
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should render the child content provided.', async () => {
|
||||
expect(el.innerText).to.eq('This is just a basic card. No image, no header, and no footer. Just your content.');
|
||||
});
|
||||
|
||||
it('should contain the class card.', async () => {
|
||||
const card = el.shadowRoot.querySelector('.card') as HTMLElement;
|
||||
expect(card.classList.value.trim()).to.eq('card');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided an element in the slot "header" to render a header', async () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlCard>(
|
||||
html`<sl-card>
|
||||
<div slot="header">Header Title</div>
|
||||
This card has a header. You can put all sorts of things in it!
|
||||
</sl-card>`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should render the child content provided.', async () => {
|
||||
expect(el.innerText).to.contain('This card has a header. You can put all sorts of things in it!');
|
||||
});
|
||||
|
||||
it('render the header content provided.', async () => {
|
||||
const header = <HTMLDivElement>el.querySelector('div[slot=header]');
|
||||
expect(header.innerText).eq('Header Title');
|
||||
});
|
||||
|
||||
it('accept "header" as an assigned child in the shadow root.', async () => {
|
||||
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=header]');
|
||||
const childNodes = slot.assignedNodes({ flatten: true });
|
||||
|
||||
expect(childNodes.length).to.eq(1);
|
||||
});
|
||||
|
||||
it('should contain the class card--has-header.', async () => {
|
||||
const card = el.shadowRoot.querySelector('.card') as HTMLElement;
|
||||
expect(card.classList.value.trim()).to.eq('card card--has-header');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided an element in the slot "footer" to render a footer', async () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlCard>(
|
||||
html`<sl-card>
|
||||
This card has a footer. You can put all sorts of things in it!
|
||||
|
||||
<div slot="footer">Footer Content</div>
|
||||
</sl-card>`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should render the child content provided.', async () => {
|
||||
expect(el.innerText).to.contain('This card has a footer. You can put all sorts of things in it!');
|
||||
});
|
||||
|
||||
it('render the footer content provided.', async () => {
|
||||
const footer = <HTMLDivElement>el.querySelector('div[slot=footer]');
|
||||
expect(footer.innerText).eq('Footer Content');
|
||||
});
|
||||
|
||||
it('accept "footer" as an assigned child in the shadow root.', async () => {
|
||||
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=footer]');
|
||||
const childNodes = slot.assignedNodes({ flatten: true });
|
||||
|
||||
expect(childNodes.length).to.eq(1);
|
||||
});
|
||||
|
||||
it('should contain the class card--has-footer.', async () => {
|
||||
const card = el.shadowRoot.querySelector('.card') as HTMLElement;
|
||||
expect(card.classList.value.trim()).to.eq('card card--has-footer');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided an element in the slot "image" to render a image', async () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlCard>(
|
||||
html`<sl-card>
|
||||
<img
|
||||
slot="image"
|
||||
src="https://images.unsplash.com/photo-1547191783-94d5f8f6d8b1?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=400&q=80"
|
||||
alt="A kitten walks towards camera on top of pallet."
|
||||
/>
|
||||
This is a kitten, but not just any kitten. This kitten likes walking along pallets.
|
||||
</sl-card>`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should render the child content provided.', async () => {
|
||||
expect(el.innerText).to.contain(
|
||||
'This is a kitten, but not just any kitten. This kitten likes walking along pallets.'
|
||||
);
|
||||
});
|
||||
|
||||
it('accept "image" as an assigned child in the shadow root.', async () => {
|
||||
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=image]');
|
||||
const childNodes = slot.assignedNodes({ flatten: true });
|
||||
|
||||
expect(childNodes.length).to.eq(1);
|
||||
});
|
||||
|
||||
it('should contain the class card--has-image.', async () => {
|
||||
const card = el.shadowRoot.querySelector('.card') as HTMLElement;
|
||||
expect(card.classList.value.trim()).to.eq('card card--has-image');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { hasSlot } from '../../internal/slot';
|
||||
import styles from './card.styles';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map.js';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined.js';
|
||||
import { live } from 'lit-html/directives/live.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 { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import styles from './checkbox.styles';
|
||||
|
||||
@@ -28,7 +28,7 @@ export default css`
|
||||
}
|
||||
|
||||
.color-picker--inline {
|
||||
border: solid 1px rgb(var(--sl-panel-border-color));
|
||||
border: solid var(--sl-panel-border-width) rgb(var(--sl-panel-border-color));
|
||||
}
|
||||
|
||||
.color-picker__grid {
|
||||
@@ -191,11 +191,14 @@ export default css`
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.color-picker__user-input sl-button-group {
|
||||
margin-left: var(--sl-spacing-small);
|
||||
}
|
||||
|
||||
.color-picker__user-input sl-button {
|
||||
min-width: 3.25rem;
|
||||
max-width: 3.25rem;
|
||||
font-size: 1rem;
|
||||
margin-left: var(--sl-spacing-small);
|
||||
}
|
||||
|
||||
.color-picker__swatches {
|
||||
@@ -299,7 +302,7 @@ export default css`
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background-color: currentColor;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25);
|
||||
box-shadow: inset 0 0 0 1px rgb(var(--sl-color-neutral-1000) / 25%);
|
||||
transition: inherit;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map.js';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined.js';
|
||||
import { live } from 'lit-html/directives/live.js';
|
||||
import { styleMap } from 'lit-html/directives/style-map.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 { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { clamp } from '../../internal/math';
|
||||
@@ -13,15 +13,19 @@ import color from 'color';
|
||||
import styles from './color-picker.styles';
|
||||
|
||||
import '../button/button';
|
||||
import '../button-group/button-group';
|
||||
import '../dropdown/dropdown';
|
||||
import '../icon/icon';
|
||||
import '../input/input';
|
||||
|
||||
const hasEyeDropper = 'EyeDropper' in window;
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
* @status stable
|
||||
*
|
||||
* @dependency sl-button
|
||||
* @dependency sl-button-group
|
||||
* @dependency sl-dropdown
|
||||
* @dependency sl-input
|
||||
*
|
||||
@@ -39,6 +43,7 @@ import '../input/input';
|
||||
* @csspart slider-handle - Hue and opacity slider handles.
|
||||
* @csspart preview - The preview color.
|
||||
* @csspart input - The text input.
|
||||
* @csspart eye-dropper-button - The toggle format button's base.
|
||||
* @csspart format-button - The toggle format button's base.
|
||||
*
|
||||
* @cssproperty --grid-width - The width of the color grid.
|
||||
@@ -570,6 +575,22 @@ export default class SlColorPicker extends LitElement {
|
||||
this.previewButton.classList.remove('color-picker__preview-color--copied');
|
||||
}
|
||||
|
||||
handleEyeDropper() {
|
||||
if (!hasEyeDropper) {
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const eyeDropper = new EyeDropper();
|
||||
|
||||
eyeDropper
|
||||
.open()
|
||||
.then((colorSelectionResult: any) => this.setColor(colorSelectionResult.sRGBHex))
|
||||
.catch(() => {
|
||||
// The user canceled, do nothing
|
||||
});
|
||||
}
|
||||
|
||||
@watch('format')
|
||||
handleFormatChange() {
|
||||
this.syncValues();
|
||||
@@ -606,6 +627,7 @@ export default class SlColorPicker extends LitElement {
|
||||
const x = this.saturation;
|
||||
const y = 100 - this.lightness;
|
||||
|
||||
// TODO - i18n for format, copy, and eye dropper buttons
|
||||
const colorPicker = html`
|
||||
<div
|
||||
part="base"
|
||||
@@ -708,6 +730,7 @@ export default class SlColorPicker extends LitElement {
|
||||
type="button"
|
||||
part="preview"
|
||||
class="color-picker__preview color-picker__transparent-bg"
|
||||
aria-label="Copy"
|
||||
style=${styleMap({
|
||||
'--preview-color': `hsla(${this.hue}deg, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})`
|
||||
})}
|
||||
@@ -730,13 +753,26 @@ export default class SlColorPicker extends LitElement {
|
||||
@sl-change=${this.handleInputChange}
|
||||
></sl-input>
|
||||
|
||||
${!this.noFormatToggle
|
||||
? html`
|
||||
<sl-button exportparts="base:format-button" @click=${this.handleFormatToggle}>
|
||||
${this.setLetterCase(this.format)}
|
||||
</sl-button>
|
||||
`
|
||||
: ''}
|
||||
<sl-button-group>
|
||||
${!this.noFormatToggle
|
||||
? html`
|
||||
<sl-button
|
||||
aria-label="Change format"
|
||||
exportparts="base:format-button"
|
||||
@click=${this.handleFormatToggle}
|
||||
>
|
||||
${this.setLetterCase(this.format)}
|
||||
</sl-button>
|
||||
`
|
||||
: ''}
|
||||
${hasEyeDropper
|
||||
? html`
|
||||
<sl-button exportparts="base:eye-dropper-button" @click=${this.handleEyeDropper}>
|
||||
<sl-icon library="system" name="eyedropper" label="Select a color from the screen"></sl-icon>
|
||||
</sl-button>
|
||||
`
|
||||
: ''}
|
||||
</sl-button-group>
|
||||
</div>
|
||||
|
||||
${this.swatches
|
||||
|
||||
43
src/components/context-menu/context-menu.styles.ts
Normal file
43
src/components/context-menu/context-menu.styles.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
::slotted(sl-menu) {
|
||||
min-width: 180px;
|
||||
background: rgb(var(--sl-panel-background-color));
|
||||
border: solid var(--sl-panel-border-width) rgb(var(--sl-panel-border-color));
|
||||
border-radius: var(--sl-border-radius-medium);
|
||||
box-shadow: var(--sl-shadow-large);
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
position: relative;
|
||||
z-index: var(--sl-z-index-dropdown);
|
||||
}
|
||||
|
||||
.context-menu__locater {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dropdown__positioner {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.context-menu__menu {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: all;
|
||||
}
|
||||
`;
|
||||
13
src/components/context-menu/context-menu.test.ts
Normal file
13
src/components/context-menu/context-menu.test.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
// import sinon from 'sinon';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlContextMenu from './context-menu';
|
||||
|
||||
describe('<sl-context-menu>', () => {
|
||||
it('should render a component', async () => {
|
||||
const el = await fixture(html` <sl-context-menu></sl-context-menu> `);
|
||||
|
||||
expect(el).to.exist;
|
||||
});
|
||||
});
|
||||
292
src/components/context-menu/context-menu.ts
Normal file
292
src/components/context-menu/context-menu.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { emit, waitForEvent } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { Instance as PopperInstance, createPopper } from '@popperjs/core/dist/esm';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { setDefaultAnimation, getAnimation } from '../../utilities/animation-registry';
|
||||
import type SlMenu from '../menu/menu';
|
||||
import styles from './context-menu.styles';
|
||||
|
||||
import '../menu/menu';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
* @status experimental
|
||||
*
|
||||
* @dependency sl-menu
|
||||
*
|
||||
* @event sl-event-name - Emitted as an example.
|
||||
*
|
||||
* @slot - Content that will activate the context menu when right-clicked.
|
||||
* @slot menu - The menu to show when the context menu is activated, an `<sl-menu>` element.
|
||||
*
|
||||
* @event sl-show - Emitted when the context menu opens.
|
||||
* @event sl-after-show - Emitted after the context menu opens and all animations are complete.
|
||||
* @event sl-hide - Emitted when the context menu closes.
|
||||
* @event sl-after-hide - Emitted after the context menu closes and all animations are complete.
|
||||
*
|
||||
* @animation contextMenu.show - The animation to use when showing the context menu.
|
||||
* @animation contextMenu.hide - The animation to use when hiding the context menu.
|
||||
*/
|
||||
@customElement('sl-context-menu')
|
||||
export default class SlContextMenu extends LitElement {
|
||||
static styles = styles;
|
||||
|
||||
@query('.context-menu') wrapper: HTMLElement;
|
||||
@query('.context-menu__locater') locater: HTMLElement;
|
||||
@query('.context-menu__menu') menu: HTMLSlotElement;
|
||||
@query('.context-menu__positioner') positioner: HTMLElement;
|
||||
|
||||
private popover: PopperInstance;
|
||||
|
||||
/**
|
||||
* The preferred placement of the context menu. Note that the actual placement may vary as needed to keep the menu
|
||||
* inside of the viewport.
|
||||
*/
|
||||
@property() placement:
|
||||
| 'top'
|
||||
| 'top-start'
|
||||
| 'top-end'
|
||||
| 'right'
|
||||
| 'right-start'
|
||||
| 'right-end'
|
||||
| 'bottom'
|
||||
| 'bottom-start'
|
||||
| 'bottom-end'
|
||||
| 'left'
|
||||
| 'left-start'
|
||||
| 'left-end' = 'bottom-start';
|
||||
|
||||
/** Disables the context menu so it won't show when triggered. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
/** The distance in pixels from which to offset the context menu away from its target. */
|
||||
@property({ type: Number }) distance = 0;
|
||||
|
||||
/** Indicates whether or not the context menu is open. You can use this in lieu of the show/hide methods. */
|
||||
@property({ type: Boolean, reflect: true }) open = false;
|
||||
|
||||
/** The distance in pixels from which to offset the context menu along its target. */
|
||||
@property({ type: Number }) skidding = 0;
|
||||
|
||||
/**
|
||||
* Enable this option to prevent the menu from being clipped when the component is placed inside a container with
|
||||
* `overflow: auto|hidden|scroll`.
|
||||
*/
|
||||
@property({ type: Boolean }) hoist = false;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.handleDocumentKeyDown = this.handleDocumentKeyDown.bind(this);
|
||||
this.handleDocumentMouseDown = this.handleDocumentMouseDown.bind(this);
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
this.menu.hidden = !this.open;
|
||||
}
|
||||
|
||||
getMenu() {
|
||||
const slot = this.menu.querySelector('slot')!;
|
||||
return slot.assignedElements({ flatten: true }).filter(el => el.tagName.toLowerCase() === 'sl-menu')[0] as SlMenu;
|
||||
}
|
||||
|
||||
async handleContextMenu(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
const targetRect = target.getBoundingClientRect();
|
||||
const wrapperRect = this.wrapper.getBoundingClientRect();
|
||||
const { offsetX, offsetY } = event;
|
||||
const x = targetRect.left + offsetX - wrapperRect.left;
|
||||
const y = targetRect.top + offsetY - wrapperRect.top;
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if (this.open) {
|
||||
await this.hide();
|
||||
}
|
||||
|
||||
this.show(x, y);
|
||||
}
|
||||
|
||||
handleDocumentKeyDown(event: KeyboardEvent) {
|
||||
const menu = this.getMenu();
|
||||
const menuItems = menu ? menu.getAllItems() : [];
|
||||
const firstMenuItem = menuItems[0];
|
||||
const lastMenuItem = menuItems[menuItems.length - 1];
|
||||
|
||||
// Close when escape is pressed
|
||||
if (event.key === 'Escape') {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// Forward key presses that don't originate from the menu to allow keyboard selection and type-to-select
|
||||
if (menu && !event.composedPath().includes(this.menu)) {
|
||||
// Focus on a menu item
|
||||
if (['ArrowDown', 'Home'].includes(event.key) && firstMenuItem) {
|
||||
event.preventDefault();
|
||||
const menu = this.getMenu();
|
||||
menu.setCurrentItem(firstMenuItem);
|
||||
firstMenuItem.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (['ArrowUp', 'End'].includes(event.key) && lastMenuItem) {
|
||||
event.preventDefault();
|
||||
menu.setCurrentItem(lastMenuItem);
|
||||
lastMenuItem.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Other keys bring focus to the menu and initiate type-to-select behavior
|
||||
const ignoredKeys = ['Tab', 'Shift', 'Meta', 'Ctrl', 'Alt'];
|
||||
if (!ignoredKeys.includes(event.key)) {
|
||||
menu.typeToSelect(event.key);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleDocumentMouseDown(event: MouseEvent) {
|
||||
const path = event.composedPath() as Array<EventTarget>;
|
||||
|
||||
//
|
||||
// Close the context menu when clicking outside of it. We use a setTimeout here because mousedown fires before
|
||||
// contextmenu and, if the menu is already open and the user-right clicks again, we want the menu to re-open in the
|
||||
// new position instead of closing.
|
||||
//
|
||||
setTimeout(() => {
|
||||
if (this.open && !path.includes(this.menu)) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleMenuSelect() {
|
||||
// Close the context menu when a menu item is selected
|
||||
this.hide();
|
||||
}
|
||||
|
||||
@watch('open', { waitUntilFirstUpdate: true })
|
||||
async handleOpenChange() {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.open) {
|
||||
// Show
|
||||
emit(this, 'sl-show');
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
document.addEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
|
||||
await stopAnimations(this);
|
||||
|
||||
this.popover = createPopper(this.locater, this.positioner, {
|
||||
placement: this.placement,
|
||||
strategy: this.hoist ? 'fixed' : 'absolute',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
boundary: 'viewport'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [this.skidding, this.distance]
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
this.menu.hidden = false;
|
||||
const { keyframes, options } = getAnimation(this, 'contextMenu.show');
|
||||
await animateTo(this.menu, keyframes, options);
|
||||
|
||||
emit(this, 'sl-after-show');
|
||||
} else {
|
||||
// Hide
|
||||
emit(this, 'sl-hide');
|
||||
document.removeEventListener('keydown', this.handleDocumentKeyDown);
|
||||
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
|
||||
await stopAnimations(this);
|
||||
const { keyframes, options } = getAnimation(this, 'contextMenu.hide');
|
||||
await animateTo(this.menu, keyframes, options);
|
||||
|
||||
this.menu.hidden = true;
|
||||
this.locater.style.top = '0px';
|
||||
this.locater.style.left = '0px';
|
||||
this.popover.destroy();
|
||||
|
||||
emit(this, 'sl-after-hide');
|
||||
}
|
||||
}
|
||||
|
||||
/** Shows the context menu */
|
||||
async show(offsetX?: number, offsetY?: number) {
|
||||
if (this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.locater.style.top = `${offsetY || 0}px`;
|
||||
this.locater.style.left = `${offsetX || 0}px`;
|
||||
this.open = true;
|
||||
|
||||
return waitForEvent(this, 'sl-after-show');
|
||||
}
|
||||
|
||||
/** Hides the dropdown panel */
|
||||
async hide() {
|
||||
if (!this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.open = false;
|
||||
|
||||
return waitForEvent(this, 'sl-after-hide');
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="context-menu">
|
||||
<slot @contextmenu=${this.handleContextMenu}></slot>
|
||||
|
||||
<div class="context-menu__locater"></div>
|
||||
|
||||
<!-- Position the menu with a wrapper since the popover makes use of translate. This let's us add animations
|
||||
on the menu without interfering with the position. -->
|
||||
<div class="context-menu__positioner">
|
||||
<div class="context-menu__menu" hidden @sl-select=${this.handleMenuSelect}>
|
||||
<slot name="menu"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
setDefaultAnimation('contextMenu.show', {
|
||||
keyframes: [
|
||||
{ opacity: 0, transform: 'scale(0.9)' },
|
||||
{ opacity: 1, transform: 'scale(1)' }
|
||||
],
|
||||
options: { duration: 50, easing: 'ease' }
|
||||
});
|
||||
|
||||
setDefaultAnimation('contextMenu.hide', {
|
||||
keyframes: [
|
||||
{ opacity: 1, transform: 'scale(1)' },
|
||||
{ opacity: 0, transform: 'scale(0.9)' }
|
||||
],
|
||||
options: { duration: 150, easing: 'ease' }
|
||||
});
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-context-menu': SlContextMenu;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ export default css`
|
||||
.details {
|
||||
border: solid 1px rgb(var(--sl-color-neutral-200));
|
||||
border-radius: var(--sl-border-radius-medium);
|
||||
background-color: rgb(var(--sl-color-neutral-0));
|
||||
overflow-anchor: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { animateTo, stopAnimations, shimKeyframesHeightAuto } from '../../internal/animate';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
@@ -22,9 +22,9 @@ let id = 0;
|
||||
* @slot summary - The details' summary. Alternatively, you can use the summary prop.
|
||||
*
|
||||
* @event sl-show - Emitted when the details opens.
|
||||
* @event sl-after-show - Emitted after the details opens and all transitions are complete.
|
||||
* @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 transitions are complete.
|
||||
* @event sl-after-hide - Emitted after the details closes and all animations are complete.
|
||||
*
|
||||
* @csspart base - The component's base wrapper.
|
||||
* @csspart header - The summary header.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map.js';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
@@ -30,9 +30,9 @@ let id = 0;
|
||||
* @slot footer - The dialog's footer, usually one or more buttons representing various options.
|
||||
*
|
||||
* @event sl-show - Emitted when the dialog opens.
|
||||
* @event sl-after-show - Emitted after the dialog opens and all transitions are complete.
|
||||
* @event sl-after-show - Emitted after the dialog opens and all animations are complete.
|
||||
* @event sl-hide - Emitted when the dialog closes.
|
||||
* @event sl-after-hide - Emitted after the dialog closes and all transitions are complete.
|
||||
* @event sl-after-hide - Emitted after the dialog closes and all animations are complete.
|
||||
* @event sl-initial-focus - Emitted when the dialog opens and the panel gains focus. Calling `event.preventDefault()`
|
||||
* will prevent focus and allow you to set it on a different element in the dialog, such as an input or button.
|
||||
* @event sl-request-close - Emitted when the user attempts to close the dialog by clicking the close button, clicking the
|
||||
|
||||
29
src/components/divider/divider.styles.ts
Normal file
29
src/components/divider/divider.styles.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { css } from 'lit';
|
||||
import componentStyles from '../../styles/component.styles';
|
||||
|
||||
export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
--color: rgb(var(--sl-panel-border-color));
|
||||
--width: var(--sl-panel-border-width);
|
||||
--spacing: var(--sl-spacing-medium);
|
||||
}
|
||||
|
||||
:host(:not([vertical])) .menu-divider {
|
||||
display: block;
|
||||
border-top: solid var(--width) var(--color);
|
||||
margin: var(--spacing) 0;
|
||||
}
|
||||
|
||||
:host([vertical]) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:host([vertical]) .menu-divider {
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
border-left: solid var(--width) var(--color);
|
||||
margin: 0 var(--spacing);
|
||||
}
|
||||
`;
|
||||
41
src/components/divider/divider.ts
Normal file
41
src/components/divider/divider.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { watch } from '../../internal/watch';
|
||||
import styles from './divider.styles';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
* @status stable
|
||||
*
|
||||
* @csspart base - The component's base wrapper.
|
||||
*
|
||||
* @cssproperty --color - The color of the divider.
|
||||
* @cssproperty --spacing - The spacing between the divider and neighboring elements.
|
||||
* @cssproperty --width - The width of the divider.
|
||||
*/
|
||||
@customElement('sl-divider')
|
||||
export default class SlDivider extends LitElement {
|
||||
static styles = styles;
|
||||
|
||||
/** Draws the divider in a vertical orientation. */
|
||||
@property({ type: Boolean, reflect: true }) vertical = false;
|
||||
|
||||
firstUpdated() {
|
||||
this.setAttribute('role', 'separator');
|
||||
}
|
||||
|
||||
@watch('vertical')
|
||||
handleVerticalChange() {
|
||||
this.setAttribute('aria-orientation', this.vertical ? 'vertical' : 'horizontal');
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` <div part="base" class="menu-divider"></div> `;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-divider': SlDivider;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map.js';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
@@ -31,9 +31,9 @@ let id = 0;
|
||||
* @slot footer - The drawer's footer, usually one or more buttons representing various options.
|
||||
*
|
||||
* @event sl-show - Emitted when the drawer opens.
|
||||
* @event sl-after-show - Emitted after the drawer opens and all transitions are complete.
|
||||
* @event sl-after-show - Emitted after the drawer opens and all animations are complete.
|
||||
* @event sl-hide - Emitted when the drawer closes.
|
||||
* @event sl-after-hide - Emitted after the drawer closes and all transitions are complete.
|
||||
* @event sl-after-hide - Emitted after the drawer closes and all animations are complete.
|
||||
* @event sl-initial-focus - Emitted when the drawer opens and the panel gains focus. Calling `event.preventDefault()` will
|
||||
* prevent focus and allow you to set it on a different element in the drawer, such as an input or button.
|
||||
* @event sl-request-close - Emitted when the user attempts to close the drawer by clicking the close button, clicking the
|
||||
|
||||
@@ -28,7 +28,7 @@ export default css`
|
||||
font-weight: var(--sl-font-weight-normal);
|
||||
color: var(--color);
|
||||
background-color: rgb(var(--sl-panel-background-color));
|
||||
border: solid 1px rgb(var(--sl-panel-border-color));
|
||||
border: solid var(--sl-panel-border-width) rgb(var(--sl-panel-border-color));
|
||||
border-radius: var(--sl-border-radius-medium);
|
||||
box-shadow: var(--sl-shadow-large);
|
||||
overflow: auto;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { Instance as PopperInstance, createPopper } from '@popperjs/core/dist/esm';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { emit } from '../../internal/event';
|
||||
@@ -79,7 +79,7 @@ export default class SlDropdown extends LitElement {
|
||||
@property({ attribute: false }) containingElement: HTMLElement;
|
||||
|
||||
/** The distance in pixels from which to offset the panel away from its trigger. */
|
||||
@property({ type: Number }) distance = 2;
|
||||
@property({ type: Number }) distance = 0;
|
||||
|
||||
/** The distance in pixels from which to offset the panel along its trigger. */
|
||||
@property({ type: Number }) skidding = 0;
|
||||
@@ -100,28 +100,6 @@ export default class SlDropdown extends LitElement {
|
||||
if (!this.containingElement) {
|
||||
this.containingElement = this;
|
||||
}
|
||||
|
||||
// Create the popover after render
|
||||
this.updateComplete.then(() => {
|
||||
this.popover = createPopper(this.trigger, this.positioner, {
|
||||
placement: this.placement,
|
||||
strategy: this.hoist ? 'fixed' : 'absolute',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
boundary: 'viewport'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [this.skidding, this.distance]
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
@@ -131,7 +109,6 @@ export default class SlDropdown extends LitElement {
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.hide();
|
||||
this.popover.destroy();
|
||||
}
|
||||
|
||||
focusOnTrigger() {
|
||||
@@ -207,40 +184,13 @@ export default class SlDropdown extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
@watch('distance')
|
||||
@watch('hoist')
|
||||
@watch('placement')
|
||||
@watch('skidding')
|
||||
handlePopoverOptionsChange() {
|
||||
if (this.popover) {
|
||||
this.popover.setOptions({
|
||||
placement: this.placement,
|
||||
strategy: this.hoist ? 'fixed' : 'absolute',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
boundary: 'viewport'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [this.skidding, this.distance]
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleTriggerClick() {
|
||||
this.open ? this.hide() : this.show();
|
||||
}
|
||||
|
||||
handleTriggerKeyDown(event: KeyboardEvent) {
|
||||
const menu = this.getMenu();
|
||||
const menuItems = menu ? ([...menu.querySelectorAll('sl-menu-item')] as SlMenuItem[]) : [];
|
||||
const menuItems = menu ? menu.getAllItems() : [];
|
||||
const firstMenuItem = menuItems[0];
|
||||
const lastMenuItem = menuItems[menuItems.length - 1];
|
||||
|
||||
@@ -262,7 +212,7 @@ export default class SlDropdown extends LitElement {
|
||||
// When up/down is pressed, we make the assumption that the user is familiar with the menu and plans to make a
|
||||
// selection. Rather than toggle the panel, we focus on the menu (if one exists) and activate the first item for
|
||||
// faster navigation.
|
||||
if (['ArrowDown', 'ArrowUp'].includes(event.key)) {
|
||||
if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {
|
||||
event.preventDefault();
|
||||
|
||||
// Show the menu if it's not already open
|
||||
@@ -271,14 +221,14 @@ export default class SlDropdown extends LitElement {
|
||||
}
|
||||
|
||||
// Focus on a menu item
|
||||
if (event.key === 'ArrowDown' && firstMenuItem) {
|
||||
if (['ArrowDown', 'Home'].includes(event.key) && firstMenuItem) {
|
||||
const menu = this.getMenu();
|
||||
menu.setCurrentItem(firstMenuItem);
|
||||
firstMenuItem.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowUp' && lastMenuItem) {
|
||||
if (['ArrowUp', 'End'].includes(event.key) && lastMenuItem) {
|
||||
menu.setCurrentItem(lastMenuItem);
|
||||
lastMenuItem.focus();
|
||||
return;
|
||||
@@ -327,7 +277,7 @@ export default class SlDropdown extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
/** Shows the dropdown panel. */
|
||||
/** Shows the dropdown panel */
|
||||
async show() {
|
||||
if (this.open) {
|
||||
return;
|
||||
@@ -352,11 +302,9 @@ export default class SlDropdown extends LitElement {
|
||||
* is activated.
|
||||
*/
|
||||
reposition() {
|
||||
if (!this.open) {
|
||||
return;
|
||||
if (this.popover) {
|
||||
this.popover.update();
|
||||
}
|
||||
|
||||
this.popover.update();
|
||||
}
|
||||
|
||||
@watch('open', { waitUntilFirstUpdate: true })
|
||||
@@ -376,7 +324,26 @@ export default class SlDropdown extends LitElement {
|
||||
document.addEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
|
||||
await stopAnimations(this);
|
||||
this.popover.update();
|
||||
|
||||
this.popover = createPopper(this.trigger, this.positioner, {
|
||||
placement: this.placement,
|
||||
strategy: this.hoist ? 'fixed' : 'absolute',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
boundary: 'viewport'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [this.skidding, this.distance]
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
this.panel.hidden = false;
|
||||
const { keyframes, options } = getAnimation(this, 'dropdown.show');
|
||||
await animateTo(this.panel, keyframes, options);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map.js';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import styles from './icon-button.styles';
|
||||
|
||||
import '../icon/icon';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { unsafeSVG } from 'lit-html/directives/unsafe-svg.js';
|
||||
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { getIconLibrary, watchIcon, unwatchIcon } from './library';
|
||||
|
||||
@@ -41,6 +41,11 @@ const icons = {
|
||||
<path d="M3.35 5.47c-.18.16-.353.322-.518.487A13.134 13.134 0 0 0 1.172 8l.195.288c.335.48.83 1.12 1.465 1.755C4.121 11.332 5.881 12.5 8 12.5c.716 0 1.39-.133 2.02-.36l.77.772A7.029 7.029 0 0 1 8 13.5C3 13.5 0 8 0 8s.939-1.721 2.641-3.238l.708.709zm10.296 8.884-12-12 .708-.708 12 12-.708.708z"/>
|
||||
</svg>
|
||||
`,
|
||||
eyedropper: `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eyedropper" viewBox="0 0 16 16">
|
||||
<path d="M13.354.646a1.207 1.207 0 0 0-1.708 0L8.5 3.793l-.646-.647a.5.5 0 1 0-.708.708L8.293 5l-7.147 7.146A.5.5 0 0 0 1 12.5v1.793l-.854.853a.5.5 0 1 0 .708.707L1.707 15H3.5a.5.5 0 0 0 .354-.146L11 7.707l1.146 1.147a.5.5 0 0 0 .708-.708l-.647-.646 3.147-3.146a1.207 1.207 0 0 0 0-1.708l-2-2zM2 12.707l7-7L10.293 7l-7 7H2v-1.293z"></path>
|
||||
</svg>
|
||||
`,
|
||||
'grip-vertical': `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-grip-vertical" viewBox="0 0 16 16">
|
||||
<path d="M7 2a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zM7 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zM7 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-3 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm-3 3a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm3 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/>
|
||||
@@ -51,6 +56,16 @@ const icons = {
|
||||
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
|
||||
</svg>
|
||||
`,
|
||||
'play-fill': `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-play-fill" viewBox="0 0 16 16">
|
||||
<path d="m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393z"></path>
|
||||
</svg>
|
||||
`,
|
||||
'pause-fill': `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pause-fill" viewBox="0 0 16 16">
|
||||
<path d="M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5zm5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5z"></path>
|
||||
</svg>
|
||||
`,
|
||||
'star-fill': `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-star-fill" viewBox="0 0 16 16">
|
||||
<path d="M3.612 15.443c-.386.198-.824-.149-.746-.592l.83-4.73L.173 6.765c-.329-.314-.158-.888.283-.95l4.898-.696L7.538.792c.197-.39.73-.39.927 0l2.184 4.327 4.898.696c.441.062.612.636.282.95l-3.522 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256z"/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit-html/directives/style-map.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { clamp } from '../../internal/math';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
|
||||
@@ -19,7 +19,9 @@ describe('<sl-include>', () => {
|
||||
});
|
||||
|
||||
it('should emit sl-error when content cannot be loaded', async () => {
|
||||
const el = await fixture<SlInclude>(html` <sl-include src="https://404"></sl-include> `);
|
||||
const el = await fixture<SlInclude>(
|
||||
html` <sl-include src="https://jsonplaceholder.typicode.com/not-found"></sl-include> `
|
||||
);
|
||||
const loadHandler = sinon.spy();
|
||||
|
||||
el.addEventListener('sl-error', loadHandler);
|
||||
|
||||
@@ -20,48 +20,71 @@ export default css`
|
||||
font-family: var(--sl-input-font-family);
|
||||
font-weight: var(--sl-input-font-weight);
|
||||
letter-spacing: var(--sl-input-letter-spacing);
|
||||
background-color: rgb(var(--sl-input-background-color));
|
||||
border: solid var(--sl-input-border-width) rgb(var(--sl-input-border-color));
|
||||
vertical-align: middle;
|
||||
overflow: hidden;
|
||||
transition: var(--sl-transition-fast) color, var(--sl-transition-fast) border, var(--sl-transition-fast) box-shadow;
|
||||
cursor: text;
|
||||
transition: var(--sl-transition-fast) color, var(--sl-transition-fast) border, var(--sl-transition-fast) box-shadow,
|
||||
var(--sl-transition-fast) background-color;
|
||||
}
|
||||
|
||||
.input:hover:not(.input--disabled) {
|
||||
/* Standard inputs */
|
||||
.input--standard {
|
||||
background-color: rgb(var(--sl-input-background-color));
|
||||
border: solid var(--sl-input-border-width) rgb(var(--sl-input-border-color));
|
||||
}
|
||||
|
||||
.input--standard:hover:not(.input--disabled) {
|
||||
background-color: rgb(var(--sl-input-background-color-hover));
|
||||
border-color: rgb(var(--sl-input-border-color-hover));
|
||||
}
|
||||
|
||||
.input:hover:not(.input--disabled) .input__control {
|
||||
color: rgb(var(--sl-input-color-hover));
|
||||
}
|
||||
|
||||
.input.input--focused:not(.input--disabled) {
|
||||
.input--standard.input--focused:not(.input--disabled) {
|
||||
background-color: rgb(var(--sl-input-background-color-focus));
|
||||
border-color: rgb(var(--sl-input-border-color-focus));
|
||||
box-shadow: var(--sl-focus-ring);
|
||||
}
|
||||
|
||||
.input.input--focused:not(.input--disabled) .input__control {
|
||||
.input--standard.input--focused:not(.input--disabled) .input__control {
|
||||
color: rgb(var(--sl-input-color-focus));
|
||||
}
|
||||
|
||||
.input.input--disabled {
|
||||
.input--standard.input--disabled {
|
||||
background-color: rgb(var(--sl-input-background-color-disabled));
|
||||
border-color: rgb(var(--sl-input-border-color-disabled));
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.input.input--disabled .input__control {
|
||||
.input--standard.input--disabled .input__control {
|
||||
color: rgb(var(--sl-input-color-disabled));
|
||||
}
|
||||
|
||||
.input.input--disabled .input__control::placeholder {
|
||||
.input--standard.input--disabled .input__control::placeholder {
|
||||
color: rgb(var(--sl-input-placeholder-color-disabled));
|
||||
}
|
||||
|
||||
/* Filled inputs */
|
||||
.input--filled {
|
||||
border: none;
|
||||
background-color: rgb(var(--sl-input-filled-background-color));
|
||||
color: rgb(var(--sl-input-color));
|
||||
}
|
||||
|
||||
.input--filled:hover:not(.input--disabled) {
|
||||
background-color: rgb(var(--sl-input-filled-background-color-hover));
|
||||
}
|
||||
|
||||
.input--filled.input--focused:not(.input--disabled) {
|
||||
background-color: rgb(var(--sl-input-filled-background-color-focus));
|
||||
box-shadow: var(--sl-focus-ring);
|
||||
}
|
||||
|
||||
.input--filled.input--disabled {
|
||||
background-color: rgb(var(--sl-input-filled-background-color-disabled));
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.input__control {
|
||||
flex: 1 1 auto;
|
||||
font-family: inherit;
|
||||
@@ -99,6 +122,10 @@ export default css`
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.input:hover:not(.input--disabled) .input__control {
|
||||
color: rgb(var(--sl-input-color-hover));
|
||||
}
|
||||
|
||||
.input__control:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { ifDefined } from 'lit-html/directives/if-defined.js';
|
||||
import { classMap } from 'lit-html/directives/class-map.js';
|
||||
import { live } from 'lit-html/directives/live.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { live } from 'lit/directives/live.js';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { getLabelledBy, renderFormControl } from '../../internal/form-control';
|
||||
@@ -71,6 +71,9 @@ export default class SlInput extends LitElement {
|
||||
/** The input's value attribute. */
|
||||
@property() value = '';
|
||||
|
||||
/** 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;
|
||||
|
||||
@@ -293,6 +296,8 @@ export default class SlInput extends LitElement {
|
||||
|
||||
// States
|
||||
'input--pill': this.pill,
|
||||
'input--standard': !this.filled,
|
||||
'input--filled': this.filled,
|
||||
'input--disabled': this.disabled,
|
||||
'input--focused': this.hasFocus,
|
||||
'input--empty': this.value?.length === 0,
|
||||
@@ -376,7 +381,6 @@ export default class SlInput extends LitElement {
|
||||
`
|
||||
: html`
|
||||
<slot name="hide-password-icon">
|
||||
${' '}
|
||||
<sl-icon name="eye" library="system"></sl-icon>
|
||||
</slot>
|
||||
`}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import styles from './menu-divider.styles';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
* @status stable
|
||||
*
|
||||
* @csspart base - The component's base wrapper.
|
||||
*/
|
||||
@customElement('sl-menu-divider')
|
||||
export default class SlMenuDivider extends LitElement {
|
||||
static styles = styles;
|
||||
|
||||
render() {
|
||||
return html` <div part="base" class="menu-divider" role="separator" aria-hidden="true"></div> `;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-menu-divider': SlMenuDivider;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { watch } from '../../internal/watch';
|
||||
import styles from './menu-item.styles';
|
||||
|
||||
|
||||
@@ -11,4 +11,8 @@ export default css`
|
||||
.menu {
|
||||
padding: var(--sl-spacing-x-small) 0;
|
||||
}
|
||||
|
||||
::slotted(sl-divider) {
|
||||
--spacing: var(--sl-spacing-x-small);
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -10,7 +10,7 @@ import styles from './menu.styles';
|
||||
* @since 2.0
|
||||
* @status stable
|
||||
*
|
||||
* @slot - The menu's content, including menu items, menu dividers, and menu labels.
|
||||
* @slot - The menu's content, including menu items, menu labels, and dividers.
|
||||
*
|
||||
* @event {{ item: SlMenuItem }} sl-select - Emitted when a menu item is selected.
|
||||
*
|
||||
|
||||
@@ -5,11 +5,6 @@ export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.menu-divider {
|
||||
border-top: solid 1px rgb(var(--sl-panel-border-color));
|
||||
margin: var(--sl-spacing-x-small) 0;
|
||||
display: contents;
|
||||
}
|
||||
`;
|
||||
13
src/components/mutation-observer/mutation-observer.test.ts
Normal file
13
src/components/mutation-observer/mutation-observer.test.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
|
||||
// import sinon from 'sinon';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlMutationObserver from './mutation-observer';
|
||||
|
||||
describe('<sl-mutation-observer>', () => {
|
||||
it('should render a component', async () => {
|
||||
const el = await fixture(html` <sl-mutation-observer></sl-mutation-observer> `);
|
||||
|
||||
expect(el).to.exist;
|
||||
});
|
||||
});
|
||||
115
src/components/mutation-observer/mutation-observer.ts
Normal file
115
src/components/mutation-observer/mutation-observer.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { emit } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import styles from './mutation-observer.styles';
|
||||
|
||||
/**
|
||||
* @since 2.0
|
||||
* @status experimental
|
||||
*
|
||||
* @event sl-mutation - Emitted when a mutation occurs.
|
||||
*
|
||||
* @slot - The content to watch for mutations.
|
||||
*/
|
||||
@customElement('sl-mutation-observer')
|
||||
export default class SlMutationObserver extends LitElement {
|
||||
static styles = styles;
|
||||
|
||||
private mutationObserver: MutationObserver;
|
||||
|
||||
/**
|
||||
* Watches for changes to attributes. If empty, all changes will be reported. To watch only specific attributes,
|
||||
* separate them by a space.
|
||||
*/
|
||||
@property({ reflect: true }) attr: string;
|
||||
|
||||
/** Indicates whether or not the attribute's previous value should be recorded when monitoring changes. */
|
||||
@property({ attribute: 'attr-old-value', type: Boolean, reflect: true }) attrOldValue = false;
|
||||
|
||||
/** Watches for changes to the character data contained within the node. */
|
||||
@property({ attribute: 'char-data', type: Boolean, reflect: true }) charData = false;
|
||||
|
||||
/** Indicates whether or not the previous value of the node's text should be recorded. */
|
||||
@property({ attribute: 'char-data-old-value', type: Boolean, reflect: true }) charDataOldValue = false;
|
||||
|
||||
/** Watches for the addition or removal of new child nodes. */
|
||||
@property({ attribute: 'child-list', type: Boolean, reflect: true }) childList = false;
|
||||
|
||||
/** Disables the observer. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.handleMutation = this.handleMutation.bind(this);
|
||||
|
||||
this.mutationObserver = new MutationObserver(this.handleMutation);
|
||||
|
||||
if (!this.disabled) {
|
||||
this.startObserver();
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.stopObserver();
|
||||
}
|
||||
|
||||
@watch('disabled')
|
||||
handleDisabledChange() {
|
||||
if (this.disabled) {
|
||||
this.stopObserver();
|
||||
} else {
|
||||
this.startObserver();
|
||||
}
|
||||
}
|
||||
|
||||
@watch('attr', { waitUntilFirstUpdate: true })
|
||||
@watch('attr-old-value', { waitUntilFirstUpdate: true })
|
||||
@watch('char-data', { waitUntilFirstUpdate: true })
|
||||
@watch('char-data-old-value', { waitUntilFirstUpdate: true })
|
||||
@watch('childList', { waitUntilFirstUpdate: true })
|
||||
handleChange() {
|
||||
this.stopObserver();
|
||||
this.startObserver();
|
||||
}
|
||||
|
||||
handleMutation(mutationList: MutationRecord[]) {
|
||||
emit(this, 'sl-mutation', {
|
||||
detail: { mutationList }
|
||||
});
|
||||
}
|
||||
|
||||
startObserver() {
|
||||
try {
|
||||
this.mutationObserver.observe(this, {
|
||||
subtree: true,
|
||||
childList: this.childList,
|
||||
attributes: typeof this.attr === 'string',
|
||||
attributeFilter: typeof this.attr === 'string' && this.attr.length > 0 ? this.attr.split(' ') : undefined,
|
||||
attributeOldValue: this.attrOldValue,
|
||||
characterData: this.charData,
|
||||
characterDataOldValue: this.charDataOldValue
|
||||
});
|
||||
} catch {
|
||||
//
|
||||
// A mutation observer was created without one of the required attributes: attr, char-data, or child-list. The
|
||||
// browser will normally throw an error, but we'll suppress that so it doesn't appear as attributes are added
|
||||
// and removed.
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
stopObserver() {
|
||||
this.mutationObserver.disconnect();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html` <slot></slot> `;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'sl-mutation-observer': SlMutationObserver;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ export default css`
|
||||
${componentStyles}
|
||||
|
||||
:host {
|
||||
--height: 16px;
|
||||
--height: 1rem;
|
||||
--track-color: rgb(var(--sl-color-neutral-500) / 20%);
|
||||
--indicator-color: rgb(var(--sl-color-primary-600));
|
||||
--label-color: rgb(var(--sl-color-neutral-0));
|
||||
@@ -18,6 +18,7 @@ export default css`
|
||||
background-color: var(--track-color);
|
||||
height: var(--height);
|
||||
border-radius: var(--sl-border-radius-pill);
|
||||
box-shadow: inset var(--sl-shadow-small);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
89
src/components/progress-bar/progress-bar.test.ts
Normal file
89
src/components/progress-bar/progress-bar.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlProgressBar from './progress-bar';
|
||||
|
||||
describe('<sl-progress-bar>', () => {
|
||||
let el: SlProgressBar;
|
||||
|
||||
describe('when provided just a value parameter', async () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlProgressBar>(html`<sl-progress-bar value="25"></sl-progress-bar>`);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a title, and value parameter', async () => {
|
||||
let base: HTMLDivElement;
|
||||
let indicator: HTMLDivElement;
|
||||
|
||||
before(async () => {
|
||||
el = await fixture<SlProgressBar>(
|
||||
html`<sl-progress-bar title="Titled Progress Ring" value="25"></sl-progress-bar>`
|
||||
);
|
||||
base = el.shadowRoot?.querySelector('[part="base"]') as HTMLDivElement;
|
||||
indicator = el.shadowRoot?.querySelector('[part="indicator"]') as HTMLDivElement;
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('uses the value parameter on the base, as aria-valuenow', async () => {
|
||||
expect(base).attribute('aria-valuenow', '25');
|
||||
});
|
||||
|
||||
it('appends a % to the value, and uses it as the the value parameter to determine the width on the "indicator" part', async () => {
|
||||
expect(indicator).attribute('style', 'width:25%;');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided an indeterminate parameter', async () => {
|
||||
let base: HTMLDivElement;
|
||||
|
||||
before(async () => {
|
||||
el = await fixture<SlProgressBar>(
|
||||
html`<sl-progress-bar title="Titled Progress Ring" indeterminate></sl-progress-bar>`
|
||||
);
|
||||
base = el.shadowRoot?.querySelector('[part="base"]') as HTMLDivElement;
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('should append a progress-bar--indeterminate class to the "base" part.', async () => {
|
||||
expect(base.classList.value.trim()).to.eq('progress-bar progress-bar--indeterminate');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a ariaLabel, and value parameter', async () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlProgressBar>(
|
||||
html`<sl-progress-bar ariaLabel="Labelled Progress Ring" value="25"></sl-progress-bar>`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a ariaLabelledBy, and value parameter', async () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlProgressBar>(
|
||||
html`
|
||||
<label id="labelledby">Progress Ring Label</label>
|
||||
<sl-progress-bar ariaLabelledBy="labelledby" value="25"></sl-progress-bar>
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,8 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit-html/directives/class-map.js';
|
||||
import { styleMap } from 'lit-html/directives/style-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import styles from './progress-bar.styles';
|
||||
|
||||
/**
|
||||
@@ -23,12 +24,15 @@ import styles from './progress-bar.styles';
|
||||
export default class SlProgressBar extends LitElement {
|
||||
static styles = styles;
|
||||
|
||||
/** The progress bar's percentage, 0 to 100. */
|
||||
@property({ type: Number, reflect: true }) percentage = 0;
|
||||
/** The current progress, 0 to 100. */
|
||||
@property({ type: Number, reflect: true }) value = 0;
|
||||
|
||||
/** When true, percentage is ignored, the label is hidden, and the progress bar is drawn in an indeterminate state. */
|
||||
@property({ type: Boolean, reflect: true }) indeterminate = false;
|
||||
|
||||
/** The progress bar's aria label. */
|
||||
@property() label = 'Progress'; // TODO - i18n
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div
|
||||
@@ -38,11 +42,13 @@ export default class SlProgressBar extends LitElement {
|
||||
'progress-bar--indeterminate': this.indeterminate
|
||||
})}
|
||||
role="progressbar"
|
||||
title=${ifDefined(this.title)}
|
||||
aria-label=${ifDefined(this.label)}
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-valuenow="${this.indeterminate ? '' : this.percentage}"
|
||||
aria-valuenow=${this.indeterminate ? 0 : this.value}
|
||||
>
|
||||
<div part="indicator" class="progress-bar__indicator" style=${styleMap({ width: this.percentage + '%' })}>
|
||||
<div part="indicator" class="progress-bar__indicator" style=${styleMap({ width: this.value + '%' })}>
|
||||
${!this.indeterminate
|
||||
? html`
|
||||
<span part="label" class="progress-bar__label">
|
||||
|
||||
68
src/components/progress-ring/progress-ring.test.ts
Normal file
68
src/components/progress-ring/progress-ring.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { expect, fixture, html } from '@open-wc/testing';
|
||||
|
||||
import '../../../dist/shoelace.js';
|
||||
import type SlProgressRing from './progress-ring';
|
||||
|
||||
describe('<sl-progress-ring>', () => {
|
||||
let el: SlProgressRing;
|
||||
|
||||
describe('when provided just a value parameter', async () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlProgressRing>(html`<sl-progress-ring value="25"></sl-progress-ring>`);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a title, and value parameter', async () => {
|
||||
let base: HTMLDivElement;
|
||||
|
||||
before(async () => {
|
||||
el = await fixture<SlProgressRing>(
|
||||
html`<sl-progress-ring title="Titled Progress Ring" value="25"></sl-progress-ring>`
|
||||
);
|
||||
base = el.shadowRoot?.querySelector('[part="base"]') as HTMLDivElement;
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
|
||||
it('uses the value parameter on the base, as aria-valuenow', async () => {
|
||||
expect(base).attribute('aria-valuenow', '25');
|
||||
});
|
||||
|
||||
it('translates the value parameter to a percentage, and uses translation on the base, as percentage css variable', async () => {
|
||||
expect(base).attribute('style', '--percentage: 0.25');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a ariaLabel, and value parameter', async () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlProgressRing>(
|
||||
html`<sl-progress-ring ariaLabel="Labelled Progress Ring" value="25"></sl-progress-ring>`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when provided a ariaLabelledBy, and value parameter', async () => {
|
||||
before(async () => {
|
||||
el = await fixture<SlProgressRing>(
|
||||
html`
|
||||
<label id="labelledby">Progress Ring Label</label>
|
||||
<sl-progress-ring ariaLabelledBy="labelledby" value="25"></sl-progress-ring>
|
||||
`
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a component that passes accessibility test.', async () => {
|
||||
await expect(el).to.be.accessible();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { LitElement, html } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators.js';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import styles from './progress-ring.styles';
|
||||
|
||||
/**
|
||||
@@ -22,8 +23,30 @@ export default class SlProgressRing extends LitElement {
|
||||
|
||||
@query('.progress-ring__indicator') indicator: SVGCircleElement;
|
||||
|
||||
/** The current progress percentage, 0 - 100. */
|
||||
@property({ type: Number, reflect: true }) percentage: number;
|
||||
@state() indicatorOffset: string;
|
||||
|
||||
/** The current progress, 0 to 100. */
|
||||
@property({ type: Number, reflect: true }) value = 0;
|
||||
|
||||
/** The progress ring's aria label. */
|
||||
@property() label = 'Progress'; // TODO - i18n
|
||||
|
||||
updated(changedProps: Map<string, any>) {
|
||||
super.updated(changedProps);
|
||||
|
||||
//
|
||||
// This block is only required for Safari because it doesn't transition the circle when the custom properties
|
||||
// change, possibly because of a mix of pixel + unitless values in the calc() function. It seems like a Safari bug,
|
||||
// but I couldn't pinpoint it so this works around the problem.
|
||||
//
|
||||
if (changedProps.has('percentage')) {
|
||||
const radius = parseFloat(getComputedStyle(this.indicator).getPropertyValue('r'));
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const offset = circumference - (this.value / 100) * circumference;
|
||||
|
||||
this.indicatorOffset = String(offset) + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
@@ -31,13 +54,15 @@ export default class SlProgressRing extends LitElement {
|
||||
part="base"
|
||||
class="progress-ring"
|
||||
role="progressbar"
|
||||
aria-label=${ifDefined(this.label)}
|
||||
aria-valuemin="0"
|
||||
aria-valuemax="100"
|
||||
aria-valuenow="${this.percentage}"
|
||||
aria-valuenow="${this.value}"
|
||||
style="--percentage: ${this.value / 100}"
|
||||
>
|
||||
<svg class="progress-ring__image">
|
||||
<circle class="progress-ring__track"></circle>
|
||||
<circle class="progress-ring__indicator" style="--percentage: ${this.percentage / 100};"></circle>
|
||||
<circle class="progress-ring__indicator" style="stroke-dashoffset: ${this.indicatorOffset}"></circle>
|
||||
</svg>
|
||||
|
||||
<span part="label" class="progress-ring__label">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user