Compare commits

...

100 Commits

Author SHA1 Message Date
Cory LaViska
0a35703f2e update ctrl/tinycolor; fixes #1542 2023-08-28 09:38:53 -04:00
Cory LaViska
ab770c566e fix spacing; #1540 (#1544) 2023-08-28 09:27:57 -04:00
Konnor Rogers
1867603225 log stderr in builds (#1543) 2023-08-25 16:20:19 -04:00
Cory LaViska
cf195da424 fix stuck search 2023-08-25 09:35:05 -04:00
Cory LaViska
0cb6aa5d12 reformat by CEM plugin 2023-08-23 15:36:19 -04:00
Cory LaViska
7e4d4c3c98 2.8.0 2023-08-23 12:55:35 -04:00
Cory LaViska
b5ef3191b7 update version 2023-08-23 12:53:47 -04:00
Konnor Rogers
f30481e229 remove unused code path (#1539) 2023-08-23 12:52:42 -04:00
Konnor Rogers
ae010c333b fix: check <slot> elements for assignedElements to allow wrapping focus-trapped elements (#1537)
* fix: internal logic for tabbable checks slotted elements

* prettier

* add better note for generators

* prettier

* fix tests

* prettier

* prettier

* fix tabbable test for safari

* prettier

* Update src/internal/tabbable.ts

Co-authored-by: Cory LaViska <cory@abeautifulsite.net>

* Update src/internal/modal.ts

Co-authored-by: Cory LaViska <cory@abeautifulsite.net>

* Update src/internal/tabbable.ts

Co-authored-by: Cory LaViska <cory@abeautifulsite.net>

---------

Co-authored-by: Cory LaViska <cory@abeautifulsite.net>
2023-08-23 11:43:48 -04:00
Konnor Rogers
43d1f9ee7a fix: use verbatimModuleSyntax and isolatedModules (#1534)
* feat: use verbatimModuleSyntax and isolatedModules

* prettier

* remove newline

* prettier
2023-08-23 10:34:40 -04:00
Cory LaViska
ec17e8736d fix component links; closes #1538 2023-08-23 09:46:23 -04:00
Cory LaViska
44b27e791e fix plop template 2023-08-23 09:29:24 -04:00
Cory LaViska
02385027db fix copy button focus 2023-08-22 17:10:01 -04:00
Cory LaViska
b311072d9b use <sl-copy-button> (#1535) 2023-08-22 17:01:00 -04:00
Cory LaViska
87ac077b0a fix empty attributes in properties table (#1536) 2023-08-22 16:59:08 -04:00
Konnor Rogers
87837df35c remove extra react component wrapper, upgrade to v2 of @lit-labs/react (#1531)
* remove extra react wrapper, upgrade to v2 of @lit-labs/react, call define in module.

* add changelog entry

* prettier
2023-08-22 11:26:54 -04:00
Konnor Rogers
5d72bbd162 remove baseUrl from tsconfig for better dev experience (#1530) 2023-08-22 10:32:15 -04:00
Cory LaViska
a4fc1c5b44 Submenus (#1527)
* [RFC] Proof-of-concept commit for submenu support

This is a Request For Comments to seek directional guidance towards
implementing the submenu slot of menu-item.

Includes:
- SubmenuController to manage event listeners on menu-item.
- Example usage in menu-item documentation.
- Trivial tests to check rendering.

Outstanding questions include:
- Accessibility concerns. E.g. where to handle 'ArrowRight',
  'ArrowLeft'?
- Should selection of menu-item denoting submenu be possible or
  customizable?
- How to parameterize contained popup?
- Implementation concerns:
  - Use of ref / id
  - delegation of some rendering to the controller
  - What to test

Related to [#620](https://github.com/shoelace-style/shoelace/issues/620).

* Update submenu-controller.ts

Removed extraneous `console.log()`.

* PoC working of ArrowRight to focus on submenu.

* Revert "PoC working of ArrowRight to focus on submenu."

(Didn't mean to publish this.)

This reverts commit be04e9a221.

* [WIP] Submenu WIP continues.

- Submenus now close on change-of-focus, not a timeout.
- Keyboard navigation support added.
- Skidding fix for better alignment.
- Submenu documentation moved to Menu page.
- Tests for accessibility, right and left arrow keys.

* Cleanup: Removed dead code and dead code comments.

* style: Eslint warnings and errors fixed. npm run verify now passes.

* fix: 2 changes to menu / submenu on-click behavior:

1. Close submenu on click explicitly, so this occurs even if the menu is
   not inside of an sl-dropdown.

2. In menu, ignore clicks that do not explicitly target a menu-item.
   Clicks that were on (e.g. a menu-border) were emitting select events.

* fix: Prevent menu's extraneous Enter / space key propagation.

Menu's handleKeyDown calls item.click (to emit the selection).
Propagating the keyboard event on Enter / space would the cause re-entry
into a submenu, so prevent the needless propagation.

* Submenu tweaks ...

- 100 ms delay when opening submenus on mouseover
- Shadows added
- Distance added to popup to have submenus overlap menu slightly.

* polish up submenu stuff

* stay highlighted when submenu is open

* update changelog

* resolve feedback

---------

Co-authored-by: Bryce Moore <bryce.moore@gmail.com>
2023-08-21 17:26:41 -04:00
Konnor Rogers
539eaded73 Update React Wrappers with Refs that work (#1526)
* fix react types for refs

* fix displayName

* fix displayName]

* attempt to fix typings for React refs

* fix bad type

* prettier

* add changelog entry

* prettier
2023-08-18 13:31:50 -04:00
Cory LaViska
93b2e78092 Merge branch 'nathangray-next' into next 2023-08-18 12:05:47 -04:00
Cory LaViska
402a00dcd3 update docs 2023-08-18 12:05:22 -04:00
Cory LaViska
b63368d5f6 Merge branch 'next' of github.com:nathangray/shoelace into nathangray-next 2023-08-18 11:23:56 -04:00
Cory LaViska
74c6d3ee36 fix tree tests; #1521 2023-08-18 11:20:14 -04:00
nathan
621aa4362b Add HTMLElement to the getTag() return type 2023-08-18 09:17:02 -06:00
Cory LaViska
c8919ad11f prettier 2023-08-18 09:55:57 -04:00
Stephen Sugden
fad76dd1a2 SlTree: separate expand/collapse and selection behaviour in 'single' mode (#1521)
* Never select tree items when clicking the chevron

This changes the behaviour of sl-tree so that clicking on the expand/collapse icon will not select/deselect the item, only toggle it's expanded state.

* Refactor: inline SlTree.syncTreeItems

This was only called from 2 places, and they each had different
behaviour anyways.

* SlTree: separate expand/collapse from selection

This makes 'multi' and 'single' mode consistent with each other, and
with native file managers.
2023-08-18 09:55:29 -04:00
nathan
b2f6499b87 Fix lint warnings 2023-08-17 13:18:51 -06:00
nathan
9520e850dd Update for path changes
see 3a61d20d93
2023-08-17 11:34:25 -06:00
Cory LaViska
4ee5271a83 Merge branch 'next' of https://github.com/shoelace-style/shoelace into next 2023-08-16 15:01:46 -04:00
Thomas Allmer
d8de7bcc51 fix(docs): Inline Form Validation Docs throw error on top level await (#1522) 2023-08-16 14:59:21 -04:00
Cory LaViska
7ee31be6d6 ignore package.json 2023-08-16 14:57:03 -04:00
Cory LaViska
9cb5ba7ac1 Radio button fix (#1524)
* fix formatting

* fix radio button spacing; fixes #1523
2023-08-16 14:51:46 -04:00
Peter Siska
c380368b61 Fix NPMDIR config (#1518)
* Fix NPMDIR config

* Add missing semi
2023-08-15 10:46:51 -04:00
Konnor Rogers
e298f7e5f4 fix broken tests for shoelace-element (#1516)
* add stub code prior to test

* fix broken test

* prettier

* prettier

* prettier
2023-08-14 11:23:00 -04:00
Cory LaViska
c743561c25 update docs 2023-08-14 10:23:59 -04:00
Alexander Krolick
e73e32fb71 Add docs on setting multiple values in select (#1508) 2023-08-14 10:21:52 -04:00
Cory LaViska
b09a48bec4 fix arg name 2023-08-14 10:02:23 -04:00
Burton Smith
aeef986cf5 JetBrains IDE Integration (#1512)
* upgrade vs code integration package

* add references

* add web-types plugin

* update reference

* run prettier

* update documentation

* run prettier

* remove test script
2023-08-14 09:34:34 -04:00
Cory LaViska
6f08f50639 2.7.0 2023-08-11 13:16:46 -04:00
Cory LaViska
8fc5f598d0 update changelog 2023-08-11 13:13:00 -04:00
Cory LaViska
1383ea3fe8 React import paths (#1507)
* fix react imports in examples

* move types to definition files

* update changelog

* update changelog
2023-08-11 13:09:44 -04:00
king8fisher
f8c37e0d14 Fix missing comma in linear-gradient (#1506) 2023-08-11 13:06:10 -04:00
Cory LaViska
cf543ef335 don't hijack key presses in text fields; fixes #1492 (#1504) 2023-08-11 11:25:46 -04:00
Cory LaViska
a3450a7d83 move emphasis 2023-08-11 11:01:37 -04:00
Cory LaViska
e80b2c9fb9 prettier 2023-08-11 11:01:00 -04:00
Alexander Krolick
8d617fb98c Expand on comment about space-separated value for sl-select (#1502) 2023-08-11 10:58:14 -04:00
Burton Smith
a6e225e47c upgrade vs code integration package (#1500)
* upgrade vs code integration package

* add references
2023-08-11 10:51:33 -04:00
Cory LaViska
e21943f4fb fix typos/whitespace 2023-08-11 10:30:40 -04:00
Cory LaViska
c36df5ecc1 <sl-copy> (#1483)
* copy updates

* Update docs/pages/components/copy.md

Co-authored-by: Thomas Allmer <d4kmor@gmail.com>

* unwrap and fix case

* copy button updates

* use bs icon

* add parts, hoist, and improve parsing a bit

* update docs

* remove comment

---------

Co-authored-by: Thomas Allmer <d4kmor@gmail.com>
2023-08-11 10:27:34 -04:00
Cory LaViska
458def7830 update bootstrap icons and fix license 2023-08-10 12:59:44 -04:00
Cory LaViska
b5d800f07a don't wrap code tags in tables 2023-08-10 11:29:25 -04:00
Cory LaViska
6551a6330b remove default assignee 2023-08-09 16:13:52 -04:00
Cory LaViska
cb5f670909 update changelog 2023-08-09 15:40:36 -04:00
Cory LaViska
5b6c1632bd update var names and use stylesheet; #1496 2023-08-09 15:38:24 -04:00
Tomas Drencak
bf15f2fb8a Toggle visibility of the clear button (#1496) 2023-08-09 15:28:30 -04:00
Konnor Rogers
31ef2f7929 remove side-effects key, update React docs for cherry-picking (#1485)
* remove side-effects, update React docs for cherry-picking

* prettier

* add PR #

* prettier

* fix react import paths

* Update docs/pages/frameworks/react.md

Co-authored-by: Cory LaViska <cory@abeautifulsite.net>

* add colons to imports

---------

Co-authored-by: Cory LaViska <cory@abeautifulsite.net>
2023-08-07 13:20:34 -04:00
Cory LaViska
8aab94f184 switch skypack to esm.sh to fix react examples 2023-08-03 15:27:10 -04:00
Cory LaViska
b7acb27c98 Revert "feat(clipboard): add new component sl-clipboard (#1473)"
This reverts commit 16f3e256b0.
2023-08-02 15:35:11 -04:00
Cory LaViska
dcbbc55f28 fix up/down focus in dropdown; closes #1472 (#1481) 2023-08-01 14:05:11 -04:00
Konnor Rogers
81dfcc2eae fix treeshaking array (#1480)
* fix treeshaking array

* fix treeshaking array

* imports to not use .component
2023-08-01 14:04:36 -04:00
Thomas Allmer
16f3e256b0 feat(clipboard): add new component sl-clipboard (#1473)
* feat(clipboard): add new component sl-clipboard

* using slots

* using a single copyStatus

* feat(clipboard): support inputs/textarea/links and shadow dom

* fix(clipboard): add area-live to announce copied

* feat(clipboard): support any component with a value property
2023-08-01 13:53:11 -04:00
Cory LaViska
75b2da9eab 2.6.0 2023-07-31 15:17:43 -04:00
Cory LaViska
9736f053d9 update version 2023-07-31 15:15:54 -04:00
Cory LaViska
d0b710c26d clear search index and other cache with cmd+shift+r 2023-07-31 15:14:25 -04:00
Cory LaViska
5b83d4d1b0 update changelog 2023-07-31 14:00:57 -04:00
Thomas Allmer
89f0f4a02c feat(details): use details and summary html tag to enable in browser searching (#1470) 2023-07-31 13:58:42 -04:00
Cory LaViska
a067ccb9e0 fix docs 2023-07-27 12:39:44 -04:00
Cory LaViska
1ccea42cca fix card borders 2023-07-26 15:20:51 -04:00
Cory LaViska
0f90dd0f54 update changelog 2023-07-25 22:20:13 -04:00
Ben Anderson
262cbc9a22 Add entry to changelog for types for react-wrapped elements (#1464) 2023-07-25 22:18:54 -04:00
Konnor Rogers
3a61d20d93 Create non-auto-registering routes (#1450)
* initial attempt at not auto defining

* add files with -

* continued work on removing auto-define

* fix component definitions

* update with new tag stuff

* fix lots of things

* fix improper scoped elements

* working through side effects

* continued react wrapper work

* update changelog

* formatting

* fixes

* update changelog

* lint / formatting

* fix version injection

* fix version injection, work on test

* fix version injection, work on test

* fix merge conflicts

* fix jsdoc null issue

* fix templates

* use exports

* working on tests

* working on registration mocking

* fix customElements test

* linting

* fix some test stuff

* clean up test

* clean up comment

* rename scopedElements to dependencies

* linting / formatting

* linting / formatting

* mark all packages external and still bundle

* set bundle false

* set bundle true

* dont minify

* fix merge conflicts

* use built shoelace-element

* fix lint errors

* prettier

* appease eslint

* appease eslint gods

* appease eslint gods

* appease eslint gods

* appease eslint gods

* add shoelace-autoloader

* move it all into 1 function

* add exportmaps note

* prettier

* add jsdelivr entrypoint

* read as utf8

* update docs with .component.js importS

* prettier
2023-07-24 13:00:07 -04:00
Cory LaViska
95f4f87eb8 update changelog 2023-07-19 15:06:25 -04:00
Cory LaViska
5b3cc0d492 add part to docs; #1460 2023-07-19 15:05:52 -04:00
Yehuda Ringler
0de39a8163 Add part to button spinner (#1460) 2023-07-19 15:04:49 -04:00
Cory LaViska
879fd7a224 wait for registration before attaching form handlers; closes #1452 2023-07-18 13:38:20 -04:00
Cory LaViska
50af138424 fix typos 2023-07-18 13:15:21 -04:00
Cory LaViska
9d592f4e08 wait longer to prevent flakiness 2023-07-18 13:13:59 -04:00
Cory LaViska
5016d27af7 remove test because we can't reliably suppress retargeted clicks 2023-07-18 13:11:08 -04:00
Chellappan
7218a19357 Replace .bind() with arrow functions in form controller,modal and slot controller (#1453) 2023-07-18 13:05:00 -04:00
Cory LaViska
33d2d4368f fix logic 2023-07-18 13:03:34 -04:00
Cory LaViska
cca40ca710 remove test because we can't reliably prevent retargeted click handlers 2023-07-18 12:58:22 -04:00
Cory LaViska
c6281859fd remove dead logic 2023-07-18 12:49:22 -04:00
Cory LaViska
956271880d fix for contained 2023-07-18 12:39:44 -04:00
Cory LaViska
201ff4efc5 fix escape key in dialog/drawer; closes #1457 2023-07-18 12:37:52 -04:00
Cory LaViska
f954233bda Revert "Move keydown handler for sl-drawer back to base div (#1459)"
This reverts commit 1e243e4257.
2023-07-18 12:12:43 -04:00
Cory LaViska
8267968b76 update output 2023-07-18 12:08:50 -04:00
Stephen Sugden
1e243e4257 Move keydown handler for sl-drawer back to base div (#1459)
* Move keydown handler for sl-drawer back to base div

This restores the stacking behaviour of drawers

See: #1457

* Autofocus panel of sl-drawer when it is open on firstUpdate
2023-07-18 11:57:16 -04:00
Cory LaViska
0b6c3a46cf Quick fixes (#1458)
* update base path docs

* fix examples

* fix broken CEM data in <sl-popup>

* Update docs/pages/getting-started/installation.md

Co-authored-by: Lindsay M <126139086+lindsaym-fa@users.noreply.github.com>

---------

Co-authored-by: Lindsay M <126139086+lindsaym-fa@users.noreply.github.com>
2023-07-17 14:05:17 -04:00
Cory LaViska
a2e58b7696 fix link 2023-07-17 10:05:00 -04:00
Cory LaViska
119d299657 remove old SPA settings 2023-07-13 17:00:23 -04:00
Cory LaViska
e8634e4178 Popup virtual elements (#1449)
* 1433: POC for comments (+ fix build.watch())

* 1433: consolidate virtualAnchor into anchor

* add virtual element examples

* update changelog

---------

Co-authored-by: Marko <marko@modelcitizen.com>
2023-07-13 16:49:57 -04:00
Cory LaViska
2e2a683d11 cleanup /index.html from search results (#1454) 2023-07-13 16:20:26 -04:00
Cory LaViska
414197acc9 unset last focused item; #1436 (#1446) 2023-07-12 14:52:13 -04:00
Ben Anderson
8fd01e1eda Add event types to react wrapper components (#1419)
* Rename SlSlideChange for consistency with other events

* Setup React event types for events used by Shoelace components

Means that consumers of Shoelace via the React wrapper will be able to
use callback methods with the correct event type, instead of having to
rely on casting and friends when using Typescript.

* Add docs demonstrating importing event types for React callbacks
2023-07-12 11:31:27 -04:00
Cory LaViska
e1ca7d1f59 Lit a11y update (#1444)
* update eslint-plugin-lit-a11y to latest

* update eslint deps

* remove aria- and role attribs from slots; closes #1422
2023-07-12 11:12:15 -04:00
Cory LaViska
f84d6939bd Doc updates (#1445)
* rename to CSS parts

* fix double dashes from merging
2023-07-11 15:23:51 -04:00
Konnor Rogers
82446e2114 Add modal tab tracking (#1403)
* add modal tab tracking

* prettier

* sort by tabindex

* sort by tabindex

* add a dialog test case for shadow roots

* add a changelog note

* add a changelog note

* prettier + test fixes

* prettier + test fixes
2023-07-07 15:32:23 -04:00
Konnor Rogers
a4f0ae9088 fix: valueAsDate now falls back to native implementation (#1399)
* fix: valueAsDate now falls back to native implementation

* changelog

* prettier

* prettier
2023-07-07 13:51:22 -04:00
Cory LaViska
fe3906f766 Don't steal focus when removing focused tree items (#1430)
* don't steal focus when removing focused tree items; #1428

* update PR link
2023-07-06 10:36:41 -04:00
Cory LaViska
c9e644f3fc Allow selecting menu items with space (#1429)
* allow selecting menu items with space; #1423

* update PR
2023-07-06 10:36:29 -04:00
291 changed files with 16625 additions and 13655 deletions

View File

@@ -92,7 +92,8 @@ module.exports = {
'@typescript-eslint/member-delimiter-style': 'warn',
'@typescript-eslint/method-signature-style': 'warn',
'@typescript-eslint/no-extraneous-class': 'error',
'@typescript-eslint/no-parameter-properties': 'error',
'@typescript-eslint/no-redundant-type-constituents': 'off',
'@typescript-eslint/parameter-properties': 'error',
'@typescript-eslint/strict-boolean-expressions': 'off'
}
},

View File

@@ -3,8 +3,7 @@ name: Bug Report
about: Create a bug report to help us fix a demonstrable problem with code in the library.
title: ''
labels: bug
assignees: claviska
assignees:
---
### Describe the bug

View File

@@ -3,8 +3,6 @@ name: Feature Request
about: Suggest an idea for this project.
title: ''
labels: feature
assignees: claviska
---
### What issue are you having?

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ docs/assets/images/sprite.svg
node_modules
src/react
cdn
web-types.json

View File

@@ -7,6 +7,7 @@ docs/search.json
src/components/icon/icons
src/react/index.ts
node_modules
package.json
package-lock.json
tsconfig.json
cdn

View File

@@ -51,6 +51,7 @@
"erroneou",
"errormessage",
"esbuild",
"exportmaps",
"exportparts",
"fieldsets",
"formaction",
@@ -98,6 +99,7 @@
"minlength",
"monospace",
"mousedown",
"mousemove",
"mouseup",
"multiselectable",
"nextjs",
@@ -151,12 +153,14 @@
"tinycolor",
"transitionend",
"treeitem",
"treeshaking",
"Triaging",
"turbolinks",
"typeof",
"unbundles",
"unbundling",
"unicons",
"unsanitized",
"unsupportive",
"valpha",
"valuenow",

View File

@@ -1,4 +1,6 @@
import { generateCustomData } from 'cem-plugin-vs-code-custom-data-generator';
import * as path from 'path';
import { customElementJetBrainsPlugin } from 'custom-element-jet-brains-integration';
import { customElementVsCodePlugin } from 'custom-element-vs-code-integration';
import { parse } from 'comment-parser';
import { pascalCase } from 'pascal-case';
import commandLineArgs from 'command-line-args';
@@ -26,7 +28,7 @@ function replace(string, terms) {
}
export default {
globs: ['src/components/**/*.ts'],
globs: ['src/components/**/*.component.ts'],
exclude: ['**/*.styles.ts', '**/*.test.ts'],
plugins: [
// Append package data
@@ -36,7 +38,34 @@ export default {
customElementsManifest.package = { name, description, version, author, homepage, license };
}
},
// Infer tag names because we no longer use @customElement decorators.
{
name: 'shoelace-infer-tag-names',
analyzePhase({ ts, node, moduleDoc }) {
switch (node.kind) {
case ts.SyntaxKind.ClassDeclaration: {
const className = node.name.getText();
const classDoc = moduleDoc?.declarations?.find(declaration => declaration.name === className);
const importPath = moduleDoc.path;
// This is kind of a best guess at components. "thing.component.ts"
if (!importPath.endsWith('.component.ts')) {
return;
}
const tagNameWithoutPrefix = path.basename(importPath, '.component.ts');
const tagName = 'sl-' + tagNameWithoutPrefix;
classDoc.tagNameWithoutPrefix = tagNameWithoutPrefix;
classDoc.tagName = tagName;
// This used to be set to true by @customElement
classDoc.customElement = true;
}
}
}
},
// Parse custom jsDoc tags
{
name: 'shoelace-custom-tags',
@@ -58,6 +87,9 @@ export default {
});
});
// This is what allows us to map JSDOC comments to ReactWrappers.
classDoc['jsDoc'] = node.jsDoc?.map(jsDoc => jsDoc.getFullText()).join('\n');
const parsed = parse(`${customComments}\n */`);
parsed[0].tags?.forEach(t => {
switch (t.tag) {
@@ -116,6 +148,7 @@ export default {
if (classDoc?.events) {
classDoc.events.forEach(event => {
event.reactName = `on${pascalCase(event.name)}`;
event.eventName = `${pascalCase(event.name)}Event`;
});
}
}
@@ -137,7 +170,7 @@ export default {
//
const terms = [
{ from: /^src\//, to: '' }, // Strip the src/ prefix
{ from: /\.(t|j)sx?$/, to: '.js' } // Convert .ts to .js
{ from: /\.component.(t|j)sx?$/, to: '.js' } // Convert .ts to .js
];
mod.path = replace(mod.path, terms);
@@ -159,9 +192,24 @@ export default {
}
},
// Generate custom VS Code data
generateCustomData({
customElementVsCodePlugin({
outdir,
cssFileName: null
cssFileName: null,
referencesTemplate: (_, tag) => [
{
name: 'Documentation',
url: `https://shoelace.style/components/${tag.replace('sl-', '')}`
}
]
}),
customElementJetBrainsPlugin({
excludeCss: true,
referencesTemplate: (_, tag) => {
return {
name: 'Documentation',
url: `https://shoelace.style/components/${tag.replace('sl-', '')}`
};
}
})
]
};

View File

@@ -84,7 +84,7 @@
<p>
To import this component as a <a href="/frameworks/react">React component</a>:
</p>
<pre><code class="language-js">import { {{ component.name }} } from '@shoelace-style/shoelace/{{ meta.npmdir }}/react';</code></pre>
<pre><code class="language-js">import {{ component.name }} from '@shoelace-style/shoelace/{{ meta.npmdir }}/react/{{ component.tagNameWithoutPrefix }}';</code></pre>
</sl-tab-panel>
</sl-tab-group>
@@ -137,15 +137,17 @@
<tr>
<td>
<code class="nowrap">{{ prop.name }}</code>
{% if prop.attribute != prop.name %}
<br>
<sl-tooltip content="This attribute is different from its property">
<small>
<code class="nowrap">
{{ prop.attribute }}
</code>
</small>
</sl-tooltip>
{% if prop.attribute | length > 0 %}
{% if prop.attribute != prop.name %}
<br>
<sl-tooltip content="This attribute is different from its property">
<small>
<code class="nowrap">
{{ prop.attribute }}
</code>
</small>
</sl-tooltip>
{% endif %}
{% endif %}
</td>
<td>
@@ -185,7 +187,7 @@
</tbody>
</table>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#properties') }}">attributes and properties</a>.</em></p>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#attributes-and-properties') }}">attributes and properties</a>.</em></p>
{% endif %}
{# Events #}
@@ -305,7 +307,7 @@
</tbody>
</table>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#component-parts') }}">customizing CSS parts</a>.</em></p>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/customizing/#css-parts') }}">customizing CSS parts</a>.</em></p>
{% endif %}
{# Animations #}
@@ -329,7 +331,7 @@
</tbody>
</table>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/usage#animations') }}">customizing animations</a>.</em></p>
<p><em>Learn more about <a href="{{ rootUrl('/getting-started/customizing#animations') }}">customizing animations</a>.</em></p>
{% endif %}
{# Dependencies #}

View File

@@ -1,3 +1,5 @@
let codeBlockId = 0;
/**
* Adds copy code buttons to code fields. The provided doc should be a document object provided by JSDOM. The same
* document will be returned with the appropriate DOM manipulations.
@@ -5,19 +7,14 @@
module.exports = function (doc) {
doc.querySelectorAll('pre > code').forEach(code => {
const pre = code.closest('pre');
const button = doc.createElement('button');
button.setAttribute('type', 'button');
button.classList.add('copy-code-button');
button.setAttribute('aria-label', 'Copy');
button.innerHTML = `
<svg class="copy-code-button__copy-icon" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-files" viewBox="0 0 16 16" part="svg">
<path d="M13 0H6a2 2 0 0 0-2 2 2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2 2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm0 13V4a2 2 0 0 0-2-2H5a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1zM3 4a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4z"></path>
</svg>
const button = doc.createElement('sl-copy-button');
<svg class="copy-code-button__copied-icon" style="display: none;" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check-lg" viewBox="0 0 16 16" part="svg">
<path d="M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06.733.733 0 0 1 1.047 0l3.052 3.093 5.4-6.425a.247.247 0 0 1 .02-.022Z"></path>
</svg>
`;
if (!code.id) {
code.id = `code-block-${++codeBlockId}`;
}
button.classList.add('copy-code-button');
button.setAttribute('from', code.id);
pre.append(button);
});

View File

@@ -1,9 +1,9 @@
(() => {
function convertModuleLinks(html) {
html = html
.replace(/@shoelace-style\/shoelace/g, `https://cdn.skypack.dev/@shoelace-style/shoelace@${shoelaceVersion}`)
.replace(/from 'react'/g, `from 'https://cdn.skypack.dev/react@${reactVersion}'`)
.replace(/from "react"/g, `from "https://cdn.skypack.dev/react@${reactVersion}"`);
.replace(/@shoelace-style\/shoelace/g, `https://esm.sh/@shoelace-style/shoelace@${shoelaceVersion}`)
.replace(/from 'react'/g, `from 'https://esm.sh/react@${reactVersion}'`)
.replace(/from "react"/g, `from "https://esm.sh/react@${reactVersion}"`);
return html;
}
@@ -191,12 +191,12 @@
if (isReact) {
htmlTemplate = '<div id="root"></div>';
jsTemplate =
`import React from 'https://cdn.skypack.dev/react@${reactVersion}';\n` +
`import ReactDOM from 'https://cdn.skypack.dev/react-dom@${reactVersion}';\n` +
`import { setBasePath } from 'https://cdn.skypack.dev/@shoelace-style/shoelace@${shoelaceVersion}/${cdndir}/utilities/base-path';\n` +
`import React from 'https://esm.sh/react@${reactVersion}';\n` +
`import ReactDOM from 'https://esm.sh/react-dom@${reactVersion}';\n` +
`import { setBasePath } from 'https://esm.sh/@shoelace-style/shoelace@${shoelaceVersion}/${cdndir}/utilities/base-path';\n` +
`\n` +
`// Set the base path for Shoelace assets\n` +
`setBasePath('https://cdn.skypack.dev/@shoelace-style/shoelace@${shoelaceVersion}/${npmdir}/')\n` +
`setBasePath('https://esm.sh/@shoelace-style/shoelace@${shoelaceVersion}/${npmdir}/')\n` +
`\n${convertModuleLinks(reactExample)}\n` +
`\n` +
`ReactDOM.render(<App />, document.getElementById('root'));`;

View File

@@ -163,32 +163,6 @@
});
})();
//
// Copy code buttons
//
(() => {
document.addEventListener('click', event => {
const button = event.target.closest('.copy-code-button');
const pre = button?.closest('pre');
const code = pre?.querySelector('code');
const copyIcon = button?.querySelector('.copy-code-button__copy-icon');
const copiedIcon = button?.querySelector('.copy-code-button__copied-icon');
if (button && code) {
navigator.clipboard.writeText(code.innerText);
copyIcon.style.display = 'none';
copiedIcon.style.display = 'inline';
button.classList.add('copy-code-button--copied');
setTimeout(() => {
copyIcon.style.display = 'inline';
copiedIcon.style.display = 'none';
button.classList.remove('copy-code-button--copied');
}, 1000);
}
});
})();
//
// Smooth links
//

View File

@@ -58,15 +58,15 @@
const clearButton = siteSearch.querySelector('.search__clear-button');
const results = siteSearch.querySelector('.search__results');
const version = document.documentElement.getAttribute('data-shoelace-version');
const animationDuration = 150;
const key = `search_${version}`;
const searchDebounce = 50;
const animationDuration = 150;
let isShowing = false;
let searchTimeout;
let searchIndex;
let map;
const loadSearchIndex = new Promise(resolve => {
const key = `search_${version}`;
const cache = localStorage.getItem(key);
const wait = 'requestIdleCallback' in window ? requestIdleCallback : requestAnimationFrame;
@@ -284,7 +284,7 @@
const a = document.createElement('a');
const displayTitle = page.title ?? '';
const displayDescription = page.description ?? '';
const displayUrl = page.url.replace(/^\//, '');
const displayUrl = page.url.replace(/^\//, '').replace(/\/$/, '');
let icon = 'file-text';
a.setAttribute('role', 'option');
@@ -357,6 +357,13 @@
}
});
// Purge cache when we press CMD+CTRL+R
document.addEventListener('keydown', event => {
if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key === 'r') {
localStorage.clear();
}
});
input.addEventListener('input', handleInput);
clearButton.addEventListener('click', handleClear);
@@ -366,4 +373,12 @@
hide();
}
});
// We're using Turbo, so when a user searches for something, visits a result, and presses the back button, the search
// UI will still be visible but not interactive. This removes the search UI when Turbo renders a page so they don't
// get trapped.
window.addEventListener('turbo:render', () => {
document.body.classList.remove('search-visible');
document.querySelectorAll('.search__overlay, .search__dialog').forEach(el => el.remove());
});
})();

View File

@@ -180,7 +180,10 @@ p {
img {
max-width: 100%;
border-radius: var(--docs-border-radius);
}
.badges img {
border-radius: var(--sl-border-radius-medium);
}
.callout img,
@@ -234,6 +237,7 @@ kbd {
border: solid 1px var(--sl-color-neutral-200);
box-shadow: inset 0 1px 0 0 var(--sl-color-neutral-0), inset 0 -1px 0 0 var(--sl-color-neutral-200);
font-family: var(--sl-font-mono);
font-size: 0.9125em;
border-radius: var(--docs-border-radius);
color: var(--sl-color-neutral-800);
padding: 0.125em 0.4em;
@@ -390,6 +394,10 @@ table td p:last-child {
overflow-x: auto;
}
.table-scroll code {
white-space: nowrap;
}
th.table-name,
th.table-event-detail {
min-width: 15ch;
@@ -498,46 +506,39 @@ pre .token.italic {
/* Copy code button */
.copy-code-button {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: 0.5rem;
right: 0.5rem;
background: var(--sl-color-neutral-0);
border-radius: calc(var(--docs-border-radius) * 0.875);
border: solid 1px var(--sl-color-neutral-200);
top: 0;
right: 0;
white-space: normal;
color: var(--sl-color-neutral-800);
text-transform: uppercase;
padding: 0.5rem;
margin: 0;
cursor: pointer;
transition: 100ms opacity, 100ms scale;
transition: 150ms opacity, 150ms scale;
}
.copy-code-button svg {
width: 1rem;
height: 1rem;
.copy-code-button::part(button) {
background-color: var(--sl-color-neutral-50);
border-radius: 0 var(--docs-border-radius) 0 var(--docs-border-radius);
padding: 0.75rem;
}
.copy-code-button::part(button):hover {
background-color: color-mix(in oklch, var(--sl-color-neutral-50), var(--sl-color-neutral-1000) 3%);
}
.copy-code-button::part(button):active {
background-color: color-mix(in oklch, var(--sl-color-neutral-50), var(--sl-color-neutral-1000) 6%);
}
pre .copy-code-button {
opacity: 0;
scale: 0.9;
scale: 0.75;
}
pre:hover .copy-code-button,
.copy-code-button:focus-visible {
.copy-code-button:focus-within {
opacity: 1;
scale: 1;
}
pre:hover .copy-code-button:hover,
pre:hover .copy-code-button--copied {
background: var(--sl-color-neutral-200);
border-color: var(--sl-color-neutral-300);
color: var(--sl-color-neutral-900);
}
/* Callouts */
.callout {
position: relative;
@@ -1058,23 +1059,29 @@ html.sidebar-open #menu-toggle {
/* Hide when not defined to prevent extra wide icon toolbar while loading */
display: none;
}
#theme-selector sl-menu {
/* Set an initial size to prevent width being initally too small when first opening on small screen width */
/* Set an initial size to prevent width being too small when first opening on small screen width */
width: 140px;
}
#theme-selector sl-button {
transition: 250ms scale ease;
}
#theme-selector sl-button::part(base) {
color: var(--sl-color-neutral-0);
}
#theme-selector sl-button::part(label) {
display: flex;
padding: 0.5rem;
}
#theme-selector sl-icon {
font-size: 1.25rem;
}
.sl-theme-dark #theme-selector sl-button::part(base) {
color: var(--sl-color-neutral-1000);
}

View File

@@ -171,7 +171,10 @@ module.exports = function (eleventyConfig) {
this.field('c'); // content
results.forEach((result, index) => {
const url = path.join('/', path.relative(eleventyConfig.dir.output, result.outputPath)).replace(/\\/g, '/');
const url = path
.join('/', path.relative(eleventyConfig.dir.output, result.outputPath))
.replace(/\\/g, '/') // convert backslashes to forward slashes
.replace(/\/index.html$/, '/'); // convert trailing /index.html to /
const doc = new JSDOM(result.content, {
// We must set a default URL so links are parsed with a hostname. Let's use a bogus TLD so we can easily
// identify which ones are internal and which ones are external.

View File

@@ -13,7 +13,8 @@ layout: component
```
```jsx:react
import { SlAlert, SlIcon } from '@shoelace-style/shoelace/dist/react';
import SlAlert from '@shoelace-style/shoelace/dist/react/alert';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
const App = () => (
<SlAlert open>
@@ -74,7 +75,8 @@ Set the `variant` attribute to change the alert's variant.
```
```jsx:react
import { SlAlert, SlIcon } from '@shoelace-style/shoelace/dist/react';
import SlAlert from '@shoelace-style/shoelace/dist/react/alert';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
const App = () => (
<>
@@ -144,7 +146,8 @@ Add the `closable` attribute to show a close button that will hide the alert.
```jsx:react
import { useState } from 'react';
import { SlAlert, SlIcon } from '@shoelace-style/shoelace/dist/react';
import SlAlert from '@shoelace-style/shoelace/dist/react/alert';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
const App = () => {
const [open, setOpen] = useState(true);
@@ -172,7 +175,7 @@ Icons are optional. Simply omit the `icon` slot if you don't want them.
```
```jsx:react
import { SlAlert } from '@shoelace-style/shoelace/dist/react';
import SlAlert from '@shoelace-style/shoelace/dist/react/alert';
const App = () => (
<SlAlert variant="primary" open>
@@ -212,7 +215,9 @@ Set the `duration` attribute to automatically hide an alert after a period of ti
```jsx:react
import { useState } from 'react';
import { SlAlert, SlButton, SlIcon } from '@shoelace-style/shoelace/dist/react';
import SlAlert from '@shoelace-style/shoelace/dist/react/alert';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
const css = `
.alert-duration sl-alert {
@@ -301,7 +306,9 @@ You should always use the `closable` attribute so users can dismiss the notifica
```jsx:react
import { useRef } from 'react';
import { SlAlert, SlButton, SlIcon } from '@shoelace-style/shoelace/dist/react';
import SlAlert from '@shoelace-style/shoelace/dist/react/alert';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
function showToast(alert) {
alert.toast();

View File

@@ -13,7 +13,7 @@ layout: component
```
```jsx:react
import { SlAnimatedImage } from '@shoelace-style/shoelace/dist/react';
import SlAnimatedImage from '@shoelace-style/shoelace/dist/react/animated-image';
const App = () => (
<SlAnimatedImage
@@ -41,7 +41,7 @@ Both GIF and WEBP images are supported.
```
```jsx:react
import { SlAnimatedImage } from '@shoelace-style/shoelace/dist/react';
import SlAnimatedImage from '@shoelace-style/shoelace/dist/react/animated-image';
const App = () => (
<SlAnimatedImage src="https://shoelace.style/assets/images/tie.webp" alt="Animation of a shoe being tied" />
@@ -64,7 +64,7 @@ To set a custom size, apply a width and/or height to the host element.
{% raw %}
```jsx:react
import { SlAnimatedImage } from '@shoelace-style/shoelace/dist/react';
import SlAnimatedImage from '@shoelace-style/shoelace/dist/react/animated-image';
const App = () => (
<SlAnimatedImage
@@ -102,7 +102,7 @@ You can change the appearance and location of the control box by targeting the `
```
```jsx:react
import { SlAnimatedImage } from '@shoelace-style/shoelace/dist/react';
import SlAnimatedImage from '@shoelace-style/shoelace/dist/react/animated-image';
const css = `
.animated-image-custom-control-box::part(control-box) {

View File

@@ -27,7 +27,7 @@ To animate an element, wrap it in `<sl-animation>` and set an animation `name`.
```
```jsx:react
import { SlAnimation } from '@shoelace-style/shoelace/dist/react';
import SlAnimation from '@shoelace-style/shoelace/dist/react/animation';
const css = `
.animation-overview .box {
@@ -173,7 +173,7 @@ Use an [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/
```jsx:react
import { useEffect, useRef, useState } from 'react';
import { SlAnimation } from '@shoelace-style/shoelace/dist/react';
import SlAnimation from '@shoelace-style/shoelace/dist/react/animation';
const css = `
.animation-scroll {
@@ -262,7 +262,7 @@ Supply your own [keyframe formats](https://developer.mozilla.org/en-US/docs/Web/
```
```jsx:react
import { SlAnimation } from '@shoelace-style/shoelace/dist/react';
import SlAnimation from '@shoelace-style/shoelace/dist/react/animation';
const css = `
.animation-keyframes .box {
@@ -329,7 +329,8 @@ Animations won't play until you apply the `play` attribute. You can omit it init
```jsx:react
import { useState } from 'react';
import { SlAnimation, SlButton } from '@shoelace-style/shoelace/dist/react';
import SlAnimation from '@shoelace-style/shoelace/dist/react/animation';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
const App = () => {
const [play, setPlay] = useState(false);

View File

@@ -12,7 +12,7 @@ By default, a generic icon will be shown. You can personalize avatars by adding
```
```jsx:react
import { SlAvatar } from '@shoelace-style/shoelace/dist/react';
import SlAvatar from '@shoelace-style/shoelace/dist/react/avatar';
const App = () => <SlAvatar label="User avatar" />;
```
@@ -37,7 +37,7 @@ Avatar images can be lazily loaded by setting the `loading` attribute to `lazy`.
```
```jsx:react
import { SlAvatar } from '@shoelace-style/shoelace/dist/react';
import SlAvatar from '@shoelace-style/shoelace/dist/react/avatar';
const App = () => (
<SlAvatar
@@ -61,7 +61,7 @@ When you don't have an image to use, you can set the `initials` attribute to sho
```
```jsx:react
import { SlAvatar } from '@shoelace-style/shoelace/dist/react';
import SlAvatar from '@shoelace-style/shoelace/dist/react/avatar';
const App = () => <SlAvatar initials="SL" label="Avatar with initials: SL" />;
```
@@ -85,7 +85,8 @@ When no image or initials are set, an icon will be shown. The default avatar sho
```
```jsx:react
import { SlAvatar, SlIcon } from '@shoelace-style/shoelace/dist/react';
import SlAvatar from '@shoelace-style/shoelace/dist/react/avatar';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
const App = () => (
<>
@@ -115,7 +116,8 @@ Avatars can be shaped using the `shape` attribute.
```
```jsx:react
import { SlAvatar, SlIcon } from '@shoelace-style/shoelace/dist/react';
import SlAvatar from '@shoelace-style/shoelace/dist/react/avatar';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
const App = () => (
<>
@@ -165,7 +167,8 @@ You can group avatars with a few lines of CSS.
```
```jsx:react
import { SlAvatar, SlIcon } from '@shoelace-style/shoelace/dist/react';
import SlAvatar from '@shoelace-style/shoelace/dist/react/avatar';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
const css = `
.avatar-group sl-avatar:not(:first-of-type) {

View File

@@ -10,7 +10,7 @@ layout: component
```
```jsx:react
import { SlBadge } from '@shoelace-style/shoelace/dist/react';
import SlBadge from '@shoelace-style/shoelace/dist/react/badge';
const App = () => <SlBadge>Badge</SlBadge>;
```
@@ -30,7 +30,7 @@ Set the `variant` attribute to change the badge's variant.
```
```jsx:react
import { SlBadge } from '@shoelace-style/shoelace/dist/react';
import SlBadge from '@shoelace-style/shoelace/dist/react/badge';
const App = () => (
<>
@@ -56,7 +56,7 @@ Use the `pill` attribute to give badges rounded edges.
```
```jsx:react
import { SlBadge } from '@shoelace-style/shoelace/dist/react';
import SlBadge from '@shoelace-style/shoelace/dist/react/badge';
const App = () => (
<>
@@ -100,7 +100,7 @@ Use the `pulse` attribute to draw attention to the badge with a subtle animation
```
```jsx:react
import { SlBadge } from '@shoelace-style/shoelace/dist/react';
import SlBadge from '@shoelace-style/shoelace/dist/react/badge';
const css = `
.badge-pulse sl-badge:not(:last-of-type) {
@@ -157,7 +157,8 @@ One of the most common use cases for badges is attaching them to buttons. To mak
{% raw %}
```jsx:react
import { SlBadge, SlButton } from '@shoelace-style/shoelace/dist/react';
import SlBadge from '@shoelace-style/shoelace/dist/react/badge';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
const App = () => (
<>
@@ -200,7 +201,11 @@ When including badges in menu items, use the `suffix` slot to make sure they're
{% raw %}
```jsx:react
import { SlBadge, SlButton, SlMenu, SlMenuItem, SlMenuLabel } from '@shoelace-style/shoelace/dist/react';
import SlBadge from '@shoelace-style/shoelace/dist/react/badge';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
import SlMenuLabel from '@shoelace-style/shoelace/dist/react/menu-label';
const App = () => (
<SlMenu

View File

@@ -17,7 +17,9 @@ layout: component
```
```jsx:react
import { SlBreadcrumb, SlBreadcrumbItem, SlIcon } from '@shoelace-style/shoelace/dist/react';
import SlBreadcrumb from '@shoelace-style/shoelace/dist/react/breadcrumb';
import SlBreadcrumbItem from '@shoelace-style/shoelace/dist/react/breadcrumb-item';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
const App = () => (
<SlBreadcrumb>

View File

@@ -17,7 +17,8 @@ Breadcrumbs are usually placed before a page's main content with the current pag
```
```jsx:react
import { SlBreadcrumb, SlBreadcrumbItem } from '@shoelace-style/shoelace/dist/react';
import SlBreadcrumb from '@shoelace-style/shoelace/dist/react/breadcrumb';
import SlBreadcrumbItem from '@shoelace-style/shoelace/dist/react/breadcrumb-item';
const App = () => (
<SlBreadcrumb>
@@ -50,7 +51,8 @@ For websites, you'll probably want to use links instead. You can make any breadc
```
```jsx:react
import { SlBreadcrumb, SlBreadcrumbItem } from '@shoelace-style/shoelace/dist/react';
import SlBreadcrumb from '@shoelace-style/shoelace/dist/react/breadcrumb';
import SlBreadcrumbItem from '@shoelace-style/shoelace/dist/react/breadcrumb-item';
const App = () => (
<SlBreadcrumb>
@@ -98,7 +100,8 @@ Use the `separator` slot to change the separator that goes between breadcrumb it
```jsx:react
import '@shoelace-style/shoelace/dist/components/icon/icon.js';
import { SlBreadcrumb, SlBreadcrumbItem } from '@shoelace-style/shoelace/dist/react';
import SlBreadcrumb from '@shoelace-style/shoelace/dist/react/breadcrumb';
import SlBreadcrumbItem from '@shoelace-style/shoelace/dist/react/breadcrumb-item';
const App = () => (
<>
@@ -146,7 +149,9 @@ Use the `prefix` slot to add content before any breadcrumb item.
```
```jsx:react
import { SlBreadcrumb, SlBreadcrumbItem, SlIcon } from '@shoelace-style/shoelace/dist/react';
import SlBreadcrumb from '@shoelace-style/shoelace/dist/react/breadcrumb';
import SlBreadcrumbItem from '@shoelace-style/shoelace/dist/react/breadcrumb-item';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
const App = () => (
<SlBreadcrumb>
@@ -176,7 +181,9 @@ Use the `suffix` slot to add content after any breadcrumb item.
```
```jsx:react
import { SlBreadcrumb, SlBreadcrumbItem, SlIcon } from '@shoelace-style/shoelace/dist/react';
import SlBreadcrumb from '@shoelace-style/shoelace/dist/react/breadcrumb';
import SlBreadcrumbItem from '@shoelace-style/shoelace/dist/react/breadcrumb-item';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
const App = () => (
<SlBreadcrumb>

View File

@@ -14,7 +14,8 @@ layout: component
```
```jsx:react
import { SlButton, SlButtonGroup } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlButtonGroup from '@shoelace-style/shoelace/dist/react/button-group';
const App = () => (
<SlButtonGroup label="Alignment">
@@ -56,7 +57,8 @@ All button sizes are supported, but avoid mixing sizes within the same button gr
```
```jsx:react
import { SlButton, SlButtonGroup } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlButtonGroup from '@shoelace-style/shoelace/dist/react/button-group';
const App = () => (
<>
@@ -132,7 +134,8 @@ Theme buttons are supported through the button's `variant` attribute.
```
```jsx:react
import { SlButton, SlButtonGroup } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlButtonGroup from '@shoelace-style/shoelace/dist/react/button-group';
const App = () => (
<>
@@ -210,7 +213,8 @@ Pill buttons are supported through the button's `pill` attribute.
```
```jsx:react
import { SlButton, SlButtonGroup } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlButtonGroup from '@shoelace-style/shoelace/dist/react/button-group';
const App = () => (
<>
@@ -279,7 +283,11 @@ Dropdowns can be placed inside button groups as long as the trigger is an `<sl-b
```
```jsx:react
import { SlButton, SlButtonGroup, SlDropdown, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlButtonGroup from '@shoelace-style/shoelace/dist/react/button-group';
import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown';
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
const App = () => (
<SlButtonGroup label="Example Button Group">
@@ -320,7 +328,11 @@ Create a split button using a button and a dropdown. Use a [visually hidden](/co
```
```jsx:react
import { SlButton, SlButtonGroup, SlDropdown, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlButtonGroup from '@shoelace-style/shoelace/dist/react/button-group';
import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown';
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
const App = () => (
<SlButtonGroup label="Example Button Group">
@@ -358,7 +370,9 @@ Buttons can be wrapped in tooltips to provide more detail when the user interact
```
```jsx:react
import { SlButton, SlButtonGroup, SlTooltip } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlButtonGroup from '@shoelace-style/shoelace/dist/react/button-group';
import SlTooltip from '@shoelace-style/shoelace/dist/react/tooltip';
const App = () => (
<>
@@ -427,7 +441,10 @@ Create interactive toolbars with button groups.
```
```jsx:react
import { SlButton, SlButtonGroup, SlIcon, SlTooltip } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlButtonGroup from '@shoelace-style/shoelace/dist/react/button-group';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
import SlTooltip from '@shoelace-style/shoelace/dist/react/tooltip';
const css = `
.button-group-toolbar sl-button-group:not(:last-of-type) {

View File

@@ -10,7 +10,7 @@ layout: component
```
```jsx:react
import { SlButton } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
const App = () => <SlButton>Button</SlButton>;
```
@@ -31,7 +31,7 @@ Use the `variant` attribute to set the button's variant.
```
```jsx:react
import { SlButton } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
const App = () => (
<>
@@ -56,7 +56,7 @@ Use the `size` attribute to change a button's size.
```
```jsx:react
import { SlButton } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
const App = () => (
<>
@@ -81,7 +81,7 @@ Use the `outline` attribute to draw outlined buttons with transparent background
```
```jsx:react
import { SlButton } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
const App = () => (
<>
@@ -118,7 +118,7 @@ Use the `pill` attribute to give buttons rounded edges.
```
```jsx:react
import { SlButton } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
const App = () => (
<>
@@ -154,7 +154,8 @@ Use the `circle` attribute to create circular icon buttons. When this attribute
```
```jsx:react
import { SlButton, SlIcon } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
const App = () => (
<>
@@ -182,7 +183,7 @@ Use the `text` variant to create text buttons that share the same size as regula
```
```jsx:react
import { SlButton } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
const App = () => (
<>
@@ -211,7 +212,7 @@ It's often helpful to have a button that works like a link. This is possible by
```
```jsx:react
import { SlButton } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
const App = () => (
<>
@@ -246,7 +247,7 @@ As expected, buttons can be given a custom width by setting the `width` attribut
{% raw %}
```jsx:react
import { SlButton } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
const App = () => (
<>
@@ -324,7 +325,8 @@ Use the `prefix` and `suffix` slots to add icons.
```
```jsx:react
import { SlButton, SlIcon } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
const App = () => (
<>
@@ -396,7 +398,7 @@ Use the `caret` attribute to add a dropdown indicator when a button will trigger
```
```jsx:react
import { SlButton } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
const App = () => (
<>
@@ -427,7 +429,7 @@ Use the `loading` attribute to make a button busy. The width will remain the sam
```
```jsx:react
import { SlButton } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
const App = () => (
<>
@@ -455,7 +457,7 @@ const App = () => (
### Disabled
Use the `disabled` attribute to disable a button. Clicks will be suppressed until the disabled state is removed.
Use the `disabled` attribute to disable a button.
```html:preview
<sl-button variant="default" disabled>Default</sl-button>
@@ -467,7 +469,7 @@ Use the `disabled` attribute to disable a button. Clicks will be suppressed unti
```
```jsx:react
import { SlButton } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
const App = () => (
<>

View File

@@ -41,7 +41,9 @@ layout: component
```
```jsx:react
import { SlButton, SlCard, SlRating } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlCard from '@shoelace-style/shoelace/dist/react/card';
import SlRating from '@shoelace-style/shoelace/dist/react/rating';
const css = `
.card-overview {
@@ -104,7 +106,7 @@ Basic cards aren't very exciting, but they can display any content you want them
```
```jsx:react
import { SlCard } from '@shoelace-style/shoelace/dist/react';
import SlCard from '@shoelace-style/shoelace/dist/react/card';
const css = `
.card-basic {
@@ -159,7 +161,8 @@ Headers can be used to display titles and more.
```
```jsx:react
import { SlCard, SlIconButton } from '@shoelace-style/shoelace/dist/react';
import SlCard from '@shoelace-style/shoelace/dist/react/card';
import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button';
const css = `
.card-header {
@@ -224,7 +227,9 @@ Footers can be used to display actions, summaries, or other relevant content.
```
```jsx:react
import { SlButton, SlCard, SlRating } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlCard from '@shoelace-style/shoelace/dist/react/card';
import SlRating from '@shoelace-style/shoelace/dist/react/rating';
const css = `
.card-footer {
@@ -277,7 +282,7 @@ Cards accept an `image` slot. The image is displayed atop the card and stretches
```
```jsx:react
import { SlCard } from '@shoelace-style/shoelace/dist/react';
import SlCard from '@shoelace-style/shoelace/dist/react/card';
const css = `
.card-image {

View File

@@ -41,7 +41,8 @@ layout: component
```
```jsx:react
import { SlCarousel, SlCarouselItem } from '@shoelace-style/shoelace/dist/react';
import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel';
import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item';
const App = () => (
<SlCarousel pagination>

View File

@@ -41,7 +41,8 @@ layout: component
```
```jsx:react
import { SlCarousel, SlCarouselItem } from '@shoelace-style/shoelace/dist/react';
import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel';
import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item';
const App = () => (
<>
@@ -123,7 +124,8 @@ Use the `pagination` attribute to show the total number of slides and the curren
```
```jsx:react
import { SlCarousel, SlCarouselItem } from '@shoelace-style/shoelace/dist/react';
import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel';
import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item';
const App = () => (
<SlCarousel pagination>
@@ -201,7 +203,8 @@ Use the `navigation` attribute to show previous and next buttons.
```
```jsx:react
import { SlCarousel, SlCarouselItem } from '@shoelace-style/shoelace/dist/react';
import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel';
import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item';
const App = () => (
<SlCarousel navigation>
@@ -279,7 +282,8 @@ By default, the carousel will not advanced beyond the first and last slides. You
```
```jsx:react
import { SlCarousel, SlCarouselItem } from '@shoelace-style/shoelace/dist/react';
import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel';
import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item';
const App = () => (
<SlCarousel loop navigation pagination>
@@ -357,7 +361,8 @@ The carousel will automatically advance when the `autoplay` attribute is used. T
```
```jsx:react
import { SlCarousel, SlCarouselItem } from '@shoelace-style/shoelace/dist/react';
import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel';
import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item';
const App = () => (
<SlCarousel autoplay loop pagination>
@@ -454,7 +459,10 @@ This example is best demonstrated using a mouse. Try clicking and dragging the s
```jsx:react
import { useState } from 'react';
import { SlCarousel, SlCarouselItem, SlDivider, SlSwitch } from '@shoelace-style/shoelace/dist/react';
import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel';
import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item';
import SlDivider from '@shoelace-style/shoelace/dist/react/divider';
import SlSwitch from '@shoelace-style/shoelace/dist/react/switch';
const App = () => {
const [isEnabled, setIsEnabled] = useState(false);
@@ -522,7 +530,8 @@ The `slides-per-page` attribute makes it possible to display multiple slides at
{% raw %}
```jsx:react
import { SlCarousel, SlCarouselItem } from '@shoelace-style/shoelace/dist/react';
import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel';
import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item';
const App = () => (
<SlCarousel navigation pagination slidesPerPage={2} slidesPerMove={2}>
@@ -614,7 +623,8 @@ The content of the carousel can be changed by adding or removing carousel items.
```jsx:react
import { useState } from 'react';
import { SlCarousel, SlCarouselItem } from '@shoelace-style/shoelace/dist/react';
import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel';
import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item';
const css = `
.dynamic-carousel {
@@ -730,7 +740,8 @@ Setting the `orientation` attribute to `vertical` will render the carousel in a
```
```jsx:react
import { SlCarousel, SlCarouselItem } from '@shoelace-style/shoelace/dist/react';
import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel';
import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item';
const css = `
.vertical {
@@ -852,7 +863,11 @@ Use the `--aspect-ratio` custom property to customize the size of the carousel's
```jsx:react
import { useState } from 'react';
import { SlCarousel, SlCarouselItem, SlDivider, SlSelect, SlOption } from '@shoelace-style/shoelace/dist/react';
import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel';
import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item';
import SlDivider from '@shoelace-style/shoelace/dist/react/divider';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
import SlOption from '@shoelace-style/shoelace/dist/react/option';
const App = () => {
const [aspectRatio, setAspectRatio] = useState('3/2');
@@ -956,7 +971,10 @@ Use the `--scroll-hint` custom property to add inline padding in horizontal caro
```jsx:react
import { useState } from 'react';
import { SlCarousel, SlCarouselItem, SlDivider, SlRange } from '@shoelace-style/shoelace/dist/react';
import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel';
import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item';
import SlDivider from '@shoelace-style/shoelace/dist/react/divider';
import SlRange from '@shoelace-style/shoelace/dist/react/range';
const App = () => (
<>
@@ -1119,7 +1137,10 @@ The carousel has a robust API that makes it possible to extend and customize. Th
```jsx:react
import { useRef } from 'react';
import { SlCarousel, SlCarouselItem, SlDivider, SlRange } from '@shoelace-style/shoelace/dist/react';
import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel';
import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item';
import SlDivider from '@shoelace-style/shoelace/dist/react/divider';
import SlRange from '@shoelace-style/shoelace/dist/react/range';
const css = `
.carousel-thumbnails {

View File

@@ -10,7 +10,7 @@ layout: component
```
```jsx:react
import { SlCheckbox } from '@shoelace-style/shoelace/dist/react';
import SlCheckbox from '@shoelace-style/shoelace/dist/react/checkbox';
const App = () => <SlCheckbox>Checkbox</SlCheckbox>;
```
@@ -30,7 +30,7 @@ Use the `checked` attribute to activate the checkbox.
```
```jsx:react
import { SlCheckbox } from '@shoelace-style/shoelace/dist/react';
import SlCheckbox from '@shoelace-style/shoelace/dist/react/checkbox';
const App = () => <SlCheckbox checked>Checked</SlCheckbox>;
```
@@ -44,7 +44,7 @@ Use the `indeterminate` attribute to make the checkbox indeterminate.
```
```jsx:react
import { SlCheckbox } from '@shoelace-style/shoelace/dist/react';
import SlCheckbox from '@shoelace-style/shoelace/dist/react/checkbox';
const App = () => <SlCheckbox indeterminate>Indeterminate</SlCheckbox>;
```
@@ -58,7 +58,7 @@ Use the `disabled` attribute to disable the checkbox.
```
```jsx:react
import { SlCheckbox } from '@shoelace-style/shoelace/dist/react';
import SlCheckbox from '@shoelace-style/shoelace/dist/react/checkbox';
const App = () => <SlCheckbox disabled>Disabled</SlCheckbox>;
```
@@ -76,7 +76,7 @@ Use the `size` attribute to change a checkbox's size.
```
```jsx:react
import { SlCheckbox } from '@shoelace-style/shoelace/dist/react';
import SlCheckbox from '@shoelace-style/shoelace/dist/react/checkbox';
const App = () => (
<>
@@ -127,7 +127,8 @@ Use the `setCustomValidity()` method to set a custom validation message. This wi
```jsx:react
import { useEffect, useRef } from 'react';
import { SlButton, SlCheckbox } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlCheckbox from '@shoelace-style/shoelace/dist/react/checkbox';
const App = () => {
const checkbox = useRef(null);

View File

@@ -10,7 +10,7 @@ layout: component
```
```jsx:react
import { SlColorPicker } from '@shoelace-style/shoelace/dist/react';
import SlColorPicker from '@shoelace-style/shoelace/dist/react/color-picker';
const App = () => <SlColorPicker label="Select a color" />;
```
@@ -30,7 +30,7 @@ Use the `value` attribute to set an initial value for the color picker.
```
```jsx:react
import { SlColorPicker } from '@shoelace-style/shoelace/dist/react';
import SlColorPicker from '@shoelace-style/shoelace/dist/react/color-picker';
const App = () => <SlColorPicker value="#4a90e2" label="Select a color" />;
```
@@ -44,7 +44,7 @@ Use the `opacity` attribute to enable the opacity slider. When this is enabled,
```
```jsx:react
import { SlColorPicker } from '@shoelace-style/shoelace/dist/react';
import SlColorPicker from '@shoelace-style/shoelace/dist/react/color-picker';
const App = () => <SlColorPicker opacity label="Select a color" />;
```
@@ -63,7 +63,7 @@ To prevent users from toggling the format themselves, add the `no-format-toggle`
```
```jsx:react
import { SlColorPicker } from '@shoelace-style/shoelace/dist/react';
import SlColorPicker from '@shoelace-style/shoelace/dist/react/color-picker';
const App = () => (
<>
@@ -90,7 +90,7 @@ Use the `swatches` attribute to add convenient presets to the color picker. Any
```
```jsx:react
import { SlColorPicker } from '@shoelace-style/shoelace/dist/react';
import SlColorPicker from '@shoelace-style/shoelace/dist/react/color-picker';
const App = () => (
<SlColorPicker
@@ -114,7 +114,7 @@ Use the `size` attribute to change the color picker's trigger size.
```
```jsx:react
import { SlColorPicker } from '@shoelace-style/shoelace/dist/react';
import SlColorPicker from '@shoelace-style/shoelace/dist/react/color-picker';
const App = () => (
<>
@@ -134,7 +134,7 @@ The color picker can be rendered inline instead of in a dropdown using the `inli
```
```jsx:react
import { SlColorPicker } from '@shoelace-style/shoelace/dist/react';
import SlColorPicker from '@shoelace-style/shoelace/dist/react/color-picker';
const App = () => <SlColorPicker inline label="Select a color" />;
```

View File

@@ -0,0 +1,258 @@
---
meta:
title: Copy Button
description: Copies data to the clipboard when the user clicks the button.
layout: component
---
```html:preview
<sl-copy-button value="Shoelace rocks!"></sl-copy-button>
```
```jsx:react
import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/copy-button';
const App = () => (
<SlCopyButton value="Shoelace rocks!" />
);
```
## Examples
### Custom Labels
Copy Buttons display feedback in a tooltip. You can customize the labels using the `copy-label`, `success-label`, and `error-label` attributes.
```html:preview
<sl-copy-button
value="Custom labels are easy"
copy-label="Click to copy"
success-label="You did it!"
error-label="Whoops, your browser doesn't support this!"
></sl-copy-button>
```
```jsx:react
import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/copy-button';
const App = () => (
<SlCopyButton
value="Custom labels are easy"
copy-label="Click to copy"
success-label="You did it!"
error-label="Whoops, your browser doesn't support this!"
/>
);
```
### Custom Icons
Use the `copy-icon`, `success-icon`, and `error-icon` slots to customize the icons that get displayed for each state. You can use [`<sl-icon>`](/components/icon) or your own images.
```html:preview
<sl-copy-button value="Copied from a custom button">
<sl-icon slot="copy-icon" name="clipboard"></sl-icon>
<sl-icon slot="success-icon" name="clipboard-check"></sl-icon>
<sl-icon slot="error-icon" name="clipboard-x"></sl-icon>
</sl-copy-button>
```
```jsx:react
import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/copy-button';
import { SlIcon } from '@shoelace-style/shoelace/dist/react/icon';
const App = () => (
<>
<SlCopyButton value="Copied from a custom button">
<SlIcon slot="copy-icon" name="clipboard" />
<SlIcon slot="success-icon" name="clipboard-check" />
<SlIcon slot="error-icon" name="clipboard-x" />
</SlCopyButton>
</>
);
```
### Copying Values From Other Elements
Normally, the data that gets copied will come from the component's `value` attribute, but you can copy data from any element within the same document by providing its `id` to the `from` attribute.
When using the `from` attribute, the element's [`textContent`](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) will be copied by default. Passing an attribute or property modifier will let you copy data from one of the element's attributes or properties instead.
To copy data from an attribute, use `from="id[attr]"` where `id` is the id of the target element and `attr` is the name of the attribute you'd like to copy. To copy data from a property, use `from="id.prop"` where `id` is the id of the target element and `prop` is the name of the property you'd like to copy.
```html:preview
<!-- Copies the span's textContent -->
<span id="my-phone">+1 (234) 456-7890</span>
<sl-copy-button from="my-phone"></sl-copy-button>
<br><br>
<!-- Copies the input's "value" property -->
<sl-input id="my-input" type="text" value="User input" style="display: inline-block; max-width: 300px;"></sl-input>
<sl-copy-button from="my-input.value"></sl-copy-button>
<br><br>
<!-- Copies the link's "href" attribute -->
<a id="my-link" href="https://shoelace.style/">Shoelace Website</a>
<sl-copy-button from="my-link[href]"></sl-copy-button>
```
```jsx:react
import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/copy-button';
import { SlInput } from '@shoelace-style/shoelace/dist/react/input';
const App = () => (
<>
{/* Copies the span's textContent */}
<span id="my-phone">+1 (234) 456-7890</span>
<SlCopyButton from="my-phone" />
<br /><br />
{/* Copies the input's "value" property */}
<SlInput id="my-input" type="text" />
<SlCopyButton from="my-input.value" />
<br /><br />
{/* Copies the link's "href" attribute */}
<a id="my-link" href="https://shoelace.style/">Shoelace Website</a>
<SlCopyButton from="my-link[href]" />
</>
);
```
### Handling Errors
A copy error will occur if the value is an empty string, if the `from` attribute points to an id that doesn't exist, or if the browser rejects the operation for any reason. When this happens, the `sl-error` event will be emitted.
This example demonstrates what happens when a copy error occurs. You can customize the error label and icon using the `error-label` attribute and the `error-icon` slot, respectively.
```html:preview
<sl-copy-button from="i-do-not-exist"></sl-copy-button>
```
```jsx:react
import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/copy-button';
const App = () => (
<SlCopyButton from="i-do-not-exist" />
);
```
### Disabled
Copy buttons can be disabled by adding the `disabled` attribute.
```html:preview
<sl-copy-button value="You can't copy me" disabled></sl-copy-button>
```
```jsx:react
import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/copy-button';
const App = () => (
<SlCopyButton value="You can't copy me" disabled />
);
```
### Changing Feedback Duration
A success indicator is briefly shown after copying. You can customize the length of time the indicator is shown using the `feedback-duration` attribute.
```html:preview
<sl-copy-button value="Shoelace rocks!" feedback-duration="250"></sl-copy-button>
```
```jsx:react
import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/copy-button';
const App = () => (
<SlCopyButton value="Shoelace rocks!" feedback-duration={250} />
);
```
### Custom Styles
You can customize the button to your liking with CSS.
```html:preview
<sl-copy-button value="I'm so stylish" class="custom-styles">
<sl-icon slot="copy-icon" name="asterisk"></sl-icon>
<sl-icon slot="success-icon" name="check-lg"></sl-icon>
<sl-icon slot="error-icon" name="x-lg"></sl-icon>
</sl-copy-button>
<style>
.custom-styles {
--success-color: white;
--error-color: white;
color: white;
}
.custom-styles::part(button) {
background-color: #ff1493;
border: solid 4px #ff7ac1;
border-right-color: #ad005c;
border-bottom-color: #ad005c;
border-radius: 0;
transition: 100ms scale ease-in-out, 100ms translate ease-in-out;
}
.custom-styles::part(button):hover {
scale: 1.1;
}
.custom-styles::part(button):active {
translate: 0 2px;
}
.custom-styles::part(button):focus-visible {
outline: dashed 2px deeppink;
outline-offset: 4px;
}
</style>
```
```jsx:react
import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/copy-button';
const css = `
.custom-styles {
--success-color: white;
--error-color: white;
color: white;
}
.custom-styles::part(button) {
background-color: #ff1493;
border: solid 4px #ff7ac1;
border-right-color: #ad005c;
border-bottom-color: #ad005c;
border-radius: 0;
transition: 100ms scale ease-in-out, 100ms translate ease-in-out;
}
.custom-styles::part(button):hover {
scale: 1.1;
}
.custom-styles::part(button):active {
translate: 0 2px;
}
.custom-styles::part(button):focus-visible {
outline: dashed 2px deeppink;
outline-offset: 4px;
}
`;
const App = () => (
<>
<SlCopyButton value="I'm so stylish" className="custom-styles" />
<style>{css}</style>
</>
);
```

View File

@@ -15,7 +15,7 @@ layout: component
```
```jsx:react
import { SlDetails } from '@shoelace-style/shoelace/dist/react';
import SlDetails from '@shoelace-style/shoelace/dist/react/details';
const App = () => (
<SlDetails summary="Toggle Me">
@@ -39,7 +39,7 @@ Use the `disable` attribute to prevent the details from expanding.
```
```jsx:react
import { SlDetails } from '@shoelace-style/shoelace/dist/react';
import SlDetails from '@shoelace-style/shoelace/dist/react/details';
const App = () => (
<SlDetails summary="Disabled" disabled>
@@ -71,7 +71,8 @@ Use the `expand-icon` and `collapse-icon` slots to change the expand and collaps
```
```jsx:react
import { SlDetails, SlIcon } from '@shoelace-style/shoelace/dist/react';
import SlDetails from '@shoelace-style/shoelace/dist/react/details';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
const css = `
sl-details.custom-icon::part(summary-icon) {

View File

@@ -27,7 +27,8 @@ layout: component
```jsx:react
import { useState } from 'react';
import { SlButton, SlDialog } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlDialog from '@shoelace-style/shoelace/dist/react/dialog';
const App = () => {
const [open, setOpen] = useState(false);
@@ -75,7 +76,8 @@ Use the `--width` custom property to set the dialog's width.
```jsx:react
import { useState } from 'react';
import { SlButton, SlDialog } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlDialog from '@shoelace-style/shoelace/dist/react/dialog';
const App = () => {
const [open, setOpen] = useState(false);
@@ -125,7 +127,8 @@ By design, a dialog's height will never exceed that of the viewport. As such, di
```jsx:react
import { useState } from 'react';
import { SlButton, SlDialog } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlDialog from '@shoelace-style/shoelace/dist/react/dialog';
const App = () => {
const [open, setOpen] = useState(false);
@@ -183,7 +186,9 @@ The header shows a functional close button by default. You can use the `header-a
```jsx:react
import { useState } from 'react';
import { SlButton, SlDialog, SlIconButton } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlDialog from '@shoelace-style/shoelace/dist/react/dialog';
import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button';
const App = () => {
const [open, setOpen] = useState(false);
@@ -244,7 +249,8 @@ You can use `event.detail.source` to determine what triggered the request to clo
```jsx:react
import { useState } from 'react';
import { SlButton, SlDialog } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlDialog from '@shoelace-style/shoelace/dist/react/dialog';
const App = () => {
const [open, setOpen] = useState(false);
@@ -296,7 +302,9 @@ By default, the dialog's panel will gain focus when opened. This allows a subseq
```jsx:react
import { useState } from 'react';
import { SlButton, SlDialog, SlInput } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlDialog from '@shoelace-style/shoelace/dist/react/dialog';
import SlInput from '@shoelace-style/shoelace/dist/react/input';
const App = () => {
const [open, setOpen] = useState(false);

View File

@@ -10,7 +10,7 @@ layout: component
```
```jsx:react
import { SlDivider } from '@shoelace-style/shoelace/dist/react';
import SlDivider from '@shoelace-style/shoelace/dist/react/divider';
const App = () => <SlDivider />;
```
@@ -28,7 +28,7 @@ Use the `--width` custom property to change the width of the divider.
{% raw %}
```jsx:react
import { SlDivider } from '@shoelace-style/shoelace/dist/react';
import SlDivider from '@shoelace-style/shoelace/dist/react/divider';
const App = () => <SlDivider style={{ '--width': '4px' }} />;
```
@@ -46,7 +46,7 @@ Use the `--color` custom property to change the color of the divider.
{% raw %}
```jsx:react
import { SlDivider } from '@shoelace-style/shoelace/dist/react';
import SlDivider from '@shoelace-style/shoelace/dist/react/divider';
const App = () => <SlDivider style={{ '--color': 'tomato' }} />;
```
@@ -68,7 +68,7 @@ Use the `--spacing` custom property to change the amount of space between the di
{% raw %}
```jsx:react
import { SlDivider } from '@shoelace-style/shoelace/dist/react';
import SlDivider from '@shoelace-style/shoelace/dist/react/divider';
const App = () => (
<>
@@ -98,7 +98,7 @@ Add the `vertical` attribute to draw the divider in a vertical orientation. The
{% raw %}
```jsx:react
import { SlDivider } from '@shoelace-style/shoelace/dist/react';
import SlDivider from '@shoelace-style/shoelace/dist/react/divider';
const App = () => (
<div
@@ -138,7 +138,9 @@ Use dividers in [menus](/components/menu) to visually group menu items.
{% raw %}
```jsx:react
import { SlDivider, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
import SlDivider from '@shoelace-style/shoelace/dist/react/divider';
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
const App = () => (
<SlMenu style={{ maxWidth: '200px' }}>

View File

@@ -27,7 +27,8 @@ layout: component
```jsx:react
import { useState } from 'react';
import { SlButton, SlDrawer } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer';
const App = () => {
const [open, setOpen] = useState(false);
@@ -73,7 +74,8 @@ By default, drawers slide in from the end. To make the drawer slide in from the
```jsx:react
import { useState } from 'react';
import { SlButton, SlDrawer } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer';
const App = () => {
const [open, setOpen] = useState(false);
@@ -117,7 +119,8 @@ To make the drawer slide in from the top, set the `placement` attribute to `top`
```jsx:react
import { useState } from 'react';
import { SlButton, SlDrawer } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer';
const App = () => {
const [open, setOpen] = useState(false);
@@ -161,7 +164,8 @@ To make the drawer slide in from the bottom, set the `placement` attribute to `b
```jsx:react
import { useState } from 'react';
import { SlButton, SlDrawer } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer';
const App = () => {
const [open, setOpen] = useState(false);
@@ -215,7 +219,8 @@ Unlike normal drawers, contained drawers are not modal. This means they do not s
```jsx:react
import { useState } from 'react';
import { SlButton, SlDrawer } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer';
const App = () => {
const [open, setOpen] = useState(false);
@@ -282,7 +287,8 @@ Use the `--size` custom property to set the drawer's size. This will be applied
```jsx:react
import { useState } from 'react';
import { SlButton, SlDrawer } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer';
const App = () => {
const [open, setOpen] = useState(false);
@@ -332,7 +338,8 @@ By design, a drawer's height will never exceed 100% of its container. As such, d
```jsx:react
import { useState } from 'react';
import { SlButton, SlDrawer } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer';
const App = () => {
const [open, setOpen] = useState(false);
@@ -389,7 +396,9 @@ The header shows a functional close button by default. You can use the `header-a
```jsx:react
import { useState } from 'react';
import { SlButton, SlDrawer, SlIconButton } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer';
import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button';
const App = () => {
const [open, setOpen] = useState(false);
@@ -445,7 +454,8 @@ You can use `event.detail.source` to determine what triggered the request to clo
```jsx:react
import { useState } from 'react';
import { SlButton, SlDrawer } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer';
const App = () => {
const [open, setOpen] = useState(false);
@@ -497,7 +507,9 @@ By default, the drawer's panel will gain focus when opened. This allows a subseq
```jsx:react
import { useState } from 'react';
import { SlButton, SlDrawer, SlInput } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer';
import SlInput from '@shoelace-style/shoelace/dist/react/input';
const App = () => {
const [open, setOpen] = useState(false);

View File

@@ -7,7 +7,7 @@ layout: component
Dropdowns consist of a trigger and a panel. By default, activating the trigger will expose the panel and interacting outside of the panel will close it.
Dropdowns are designed to work well with [menus](/components/menu) to provide a list of options the user can select from. However, dropdowns can also be used in lower-level applications (e.g. [color picker](/components/color-picker) and [select](/components/select)). The API gives you complete control over showing, hiding, and positioning the panel.
Dropdowns are designed to work well with [menus](/components/menu) to provide a list of options the user can select from. However, dropdowns can also be used in lower-level applications (e.g. [color picker](/components/color-picker)). The API gives you complete control over showing, hiding, and positioning the panel.
```html:preview
<sl-dropdown>
@@ -33,7 +33,12 @@ Dropdowns are designed to work well with [menus](/components/menu) to provide a
```
```jsx:react
import { SlButton, SlDivider, SlDropdown, SlIcon, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlDivider from '@shoelace-style/shoelace/dist/react/divider';
import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
const App = () => (
<SlDropdown>
@@ -93,7 +98,10 @@ When dropdowns are used with [menus](/components/menu), you can listen for the [
```
```jsx:react
import { SlButton, SlDropdown, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown';
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
const App = () => {
function handleSelect(event) {
@@ -143,7 +151,10 @@ Alternatively, you can listen for the `click` event on individual menu items. No
```
```jsx:react
import { SlButton, SlDropdown, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown';
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
const App = () => {
function handleCut() {
@@ -192,7 +203,11 @@ The preferred placement of the dropdown can be set with the `placement` attribut
```
```jsx:react
import { SlButton, SlDivider, SlDropdown, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlDivider from '@shoelace-style/shoelace/dist/react/divider';
import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown';
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
const App = () => (
<SlDropdown placement="top-start">
@@ -230,7 +245,11 @@ The distance from the panel to the trigger can be customized using the `distance
```
```jsx:react
import { SlButton, SlDivider, SlDropdown, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlDivider from '@shoelace-style/shoelace/dist/react/divider';
import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown';
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
const App = () => (
<SlDropdown distance={30}>
@@ -268,7 +287,11 @@ The offset of the panel along the trigger can be customized using the `skidding`
```
```jsx:react
import { SlButton, SlDivider, SlDropdown, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlDivider from '@shoelace-style/shoelace/dist/react/divider';
import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown';
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
const App = () => (
<SlDropdown skidding={30}>
@@ -287,6 +310,96 @@ const App = () => (
);
```
### Submenus
To create a submenu, nest an `<sl-menu slot="submenu">` element in a [menu item](/components/menu-item).
```html:preview
<sl-dropdown>
<sl-button slot="trigger" caret>Edit</sl-button>
<sl-menu style="max-width: 200px;">
<sl-menu-item value="undo">Undo</sl-menu-item>
<sl-menu-item value="redo">Redo</sl-menu-item>
<sl-divider></sl-divider>
<sl-menu-item value="cut">Cut</sl-menu-item>
<sl-menu-item value="copy">Copy</sl-menu-item>
<sl-menu-item value="paste">Paste</sl-menu-item>
<sl-divider></sl-divider>
<sl-menu-item>
Find
<sl-menu slot="submenu">
<sl-menu-item value="find">Find…</sl-menu-item>
<sl-menu-item value="find-previous">Find Next</sl-menu-item>
<sl-menu-item value="find-next">Find Previous</sl-menu-item>
</sl-menu>
</sl-menu-item>
<sl-menu-item>
Transformations
<sl-menu slot="submenu">
<sl-menu-item value="uppercase">Make uppercase</sl-menu-item>
<sl-menu-item value="lowercase">Make lowercase</sl-menu-item>
<sl-menu-item value="capitalize">Capitalize</sl-menu-item>
</sl-menu>
</sl-menu-item>
</sl-menu>
</sl-dropdown>
```
```jsx:react
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlDivider from '@shoelace-style/shoelace/dist/react/divider';
import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown';
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
const css = `
.dropdown-hoist {
border: solid 2px var(--sl-panel-border-color);
padding: var(--sl-spacing-medium);
overflow: hidden;
}
`;
const App = () => (
<>
<SlDropdown>
<SlButton slot="trigger" caret>Edit</SlButton>
<SlMenu style="max-width: 200px;">
<SlMenuItem value="undo">Undo</SlMenuItem>
<SlMenuItem value="redo">Redo</SlMenuItem>
<SlDivider />
<SlMenuItem value="cut">Cut</SlMenuItem>
<SlMenuItem value="copy">Copy</SlMenuItem>
<SlMenuItem value="paste">Paste</SlMenuItem>
<SlDivider />
<SlMenuItem>
Find
<SlMenu slot="submenu">
<SlMenuItem value="find">Find…</SlMenuItem>
<SlMenuItem value="find-previous">Find Next</SlMenuItem>
<SlMenuItem value="find-next">Find Previous</SlMenuItem>
</SlMenu>
</SlMenuItem>
<SlMenuItem>
Transformations
<SlMenu slot="submenu">
<SlMenuItem value="uppercase">Make uppercase</SlMenuItem>
<SlMenuItem value="lowercase">Make lowercase</SlMenuItem>
<SlMenuItem value="capitalize">Capitalize</SlMenuItem>
</SlMenu>
</SlMenuItem>
</SlMenu>
</SlDropdown>
</>
);
```
:::warning
As a UX best practice, avoid using more than one level of submenu when possible.
:::
### Hoisting
Dropdown panels will be clipped if they're inside a container that has `overflow: auto|hidden`. The `hoist` attribute forces the panel to use a fixed positioning strategy, allowing it to break out of the container. In this case, the panel will be positioned relative to its [containing block](https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#Identifying_the_containing_block), which is usually the viewport unless an ancestor uses a `transform`, `perspective`, or `filter`. [Refer to this page](https://developer.mozilla.org/en-US/docs/Web/CSS/position#fixed) for more details.
@@ -323,7 +436,11 @@ Dropdown panels will be clipped if they're inside a container that has `overflow
```
```jsx:react
import { SlButton, SlDivider, SlDropdown, SlIcon, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlDivider from '@shoelace-style/shoelace/dist/react/divider';
import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown';
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
const css = `
.dropdown-hoist {

View File

@@ -24,7 +24,9 @@ layout: component
```jsx:react
import { useState } from 'react';
import { SlButton, SlFormatBytes, SlInput } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlFormatBytes from '@shoelace-style/shoelace/dist/react/format-bytes';
import SlInput from '@shoelace-style/shoelace/dist/react/input';
const App = () => {
const [value, setValue] = useState(1000);
@@ -62,7 +64,7 @@ Set the `value` attribute to a number to get the value in bytes.
```
```jsx:react
import { SlFormatBytes } from '@shoelace-style/shoelace/dist/react';
import SlFormatBytes from '@shoelace-style/shoelace/dist/react/format-bytes';
const App = () => (
<>
@@ -89,7 +91,7 @@ To get the value in bits, set the `unit` attribute to `bit`.
```
```jsx:react
import { SlFormatBytes } from '@shoelace-style/shoelace/dist/react';
import SlFormatBytes from '@shoelace-style/shoelace/dist/react/format-bytes';
const App = () => (
<>
@@ -116,7 +118,7 @@ Use the `lang` attribute to set the number formatting locale.
```
```jsx:react
import { SlFormatBytes } from '@shoelace-style/shoelace/dist/react';
import SlFormatBytes from '@shoelace-style/shoelace/dist/react/format-bytes';
const App = () => (
<>

View File

@@ -13,7 +13,7 @@ Localization is handled by the browser's [`Intl.DateTimeFormat` API](https://dev
```
```jsx:react
import { SlFormatDate } from '@shoelace-style/shoelace/dist/react';
import SlFormatDate from '@shoelace-style/shoelace/dist/react/format-date';
const App = () => <SlFormatDate date="2020-07-15T09:17:00-04:00" />;
```
@@ -51,7 +51,7 @@ Formatting options are based on those found in the [`Intl.DateTimeFormat` API](h
```
```jsx:react
import { SlFormatDate } from '@shoelace-style/shoelace/dist/react';
import SlFormatDate from '@shoelace-style/shoelace/dist/react/format-date';
const App = () => (
<>
@@ -91,7 +91,7 @@ By default, the browser will determine whether to use 12-hour or 24-hour time. T
```
```jsx:react
import { SlFormatDate } from '@shoelace-style/shoelace/dist/react';
import SlFormatDate from '@shoelace-style/shoelace/dist/react/format-date';
const App = () => (
<>
@@ -113,7 +113,7 @@ Russian: <sl-format-date lang="ru"></sl-format-date>
```
```jsx:react
import { SlFormatDate } from '@shoelace-style/shoelace/dist/react';
import SlFormatDate from '@shoelace-style/shoelace/dist/react/format-date';
const App = () => (
<>

View File

@@ -27,7 +27,8 @@ Localization is handled by the browser's [`Intl.NumberFormat` API](https://devel
```jsx:react
import { useState } from 'react';
import { SlFormatNumber, SlInput } from '@shoelace-style/shoelace/dist/react';
import SlFormatNumber from '@shoelace-style/shoelace/dist/react/format-number';
import SlInput from '@shoelace-style/shoelace/dist/react/input';
const App = () => {
const [value, setValue] = useState(1000);
@@ -66,7 +67,7 @@ To get the value as a percent, set the `type` attribute to `percent`.
```
```jsx:react
import { SlFormatNumber } from '@shoelace-style/shoelace/dist/react';
import SlFormatNumber from '@shoelace-style/shoelace/dist/react/format-number';
const App = () => (
<>
@@ -94,7 +95,7 @@ Russian: <sl-format-number value="2000" lang="ru" minimum-fraction-digits="2"></
```
```jsx:react
import { SlFormatNumber } from '@shoelace-style/shoelace/dist/react';
import SlFormatNumber from '@shoelace-style/shoelace/dist/react/format-number';
const App = () => (
<>
@@ -120,7 +121,7 @@ To format a number as a monetary value, set the `type` attribute to `currency` a
```
```jsx:react
import { SlFormatNumber } from '@shoelace-style/shoelace/dist/react';
import SlFormatNumber from '@shoelace-style/shoelace/dist/react/format-number';
const App = () => (
<>

View File

@@ -12,7 +12,7 @@ For a full list of icons that come bundled with Shoelace, refer to the [icon com
```
```jsx:react
import { SlIconButton } from '@shoelace-style/shoelace/dist/react';
import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button';
const App = () => <SlIconButton name="gear" label="Settings" />;
```
@@ -32,7 +32,7 @@ Icon buttons inherit their parent element's `font-size`.
{% raw %}
```jsx:react
import { SlIconButton } from '@shoelace-style/shoelace/dist/react';
import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button';
const App = () => (
<>
@@ -73,7 +73,7 @@ Icon buttons are designed to have a uniform appearance, so their color is not in
```
```jsx:react
import { SlIconButton } from '@shoelace-style/shoelace/dist/react';
import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button';
const css = `
.icon-button-color sl-icon-button::part(base) {
@@ -112,7 +112,7 @@ Use the `href` attribute to convert the button to a link.
```
```jsx:react
import { SlIconButton } from '@shoelace-style/shoelace/dist/react';
import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button';
const App = () => <SlIconButton name="gear" label="Settings" href="https://example.com" target="_blank" />;
```
@@ -128,7 +128,8 @@ Wrap a tooltip around an icon button to provide contextual information to the us
```
```jsx:react
import { SlIconButton, SlTooltip } from '@shoelace-style/shoelace/dist/react';
import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button';
import SlTooltip from '@shoelace-style/shoelace/dist/react/tooltip';
const App = () => (
<SlTooltip content="Settings">
@@ -146,7 +147,7 @@ Use the `disabled` attribute to disable the icon button.
```
```jsx:react
import { SlIconButton } from '@shoelace-style/shoelace/dist/react';
import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button';
const App = () => <SlIconButton name="gear" label="Settings" disabled />;
```

View File

@@ -8,7 +8,7 @@ layout: component
Shoelace comes bundled with over 1,500 icons courtesy of the [Bootstrap Icons](https://icons.getbootstrap.com/) project. These icons are part of the `default` icon library. If you prefer, you can register [custom icon libraries](#icon-libraries) as well.
:::tip
Depending on how you're loading Shoelace, you may need to copy icon assets and/or [set the base path](getting-started/installation#setting-the-base-path) so Shoelace knows where to load them from. Otherwise, icons may not appear and you'll see 404 Not Found errors in the dev console.
Depending on how you're loading Shoelace, you may need to copy icon assets and/or [set the base path](/getting-started/installation/#setting-the-base-path) so Shoelace knows where to load them from. Otherwise, icons may not appear and you'll see 404 Not Found errors in the dev console.
:::
## Default Icons
@@ -70,7 +70,7 @@ Icons inherit their color from the current text color. Thus, you can set the `co
{% raw %}
```jsx:react
import { SlIcon } from '@shoelace-style/shoelace/dist/react';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
const App = () => (
<>
@@ -132,7 +132,7 @@ Icons are sized relative to the current font size. To change their size, set the
{% raw %}
```jsx:react
import { SlIcon } from '@shoelace-style/shoelace/dist/react';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
const App = () => (
<div style={{ fontSize: '32px' }}>
@@ -167,7 +167,7 @@ For non-decorative icons, use the `label` attribute to announce it to assistive
```
```jsx:react
import { SlIcon } from '@shoelace-style/shoelace/dist/react';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
const App = () => <SlIcon name="star-fill" label="Add to favorites" />;
```
@@ -183,7 +183,7 @@ Custom icons can be loaded individually with the `src` attribute. Only SVGs on a
{% raw %}
```jsx:react
import { SlIcon } from '@shoelace-style/shoelace/dist/react';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
const App = () => <SlIcon src="https://shoelace.style/assets/images/shoe.svg" style={{ fontSize: '8rem' }}></SlIcon>;
```
@@ -645,9 +645,7 @@ When using sprite sheets, the `sl-load` and `sl-error` events will not fire.
:::
:::danger
For security reasons, browsers may apply the same-origin policy on `<use>` elements located in the `<sl-icon>` shadow dom and
may refuse to load a cross-origin URL. There is currently no defined way to set a cross-origin policy for `<use>` elements.
For this reason, sprite sheets should only be used if you're self-hosting them.
For security reasons, browsers may apply the same-origin policy on `<use>` elements located in the `<sl-icon>` shadow DOM and may refuse to load a cross-origin URL. There is currently no defined way to set a cross-origin policy for `<use>` elements. For this reason, sprite sheets should only be used if you're self-hosting them.
:::
```html:preview

View File

@@ -23,7 +23,7 @@ For best results, use images that share the same dimensions. The slider can be c
```
```jsx:react
import { SlImageComparer } from '@shoelace-style/shoelace/dist/react';
import SlImageComparer from '@shoelace-style/shoelace/dist/react/image-comparer';
const App = () => (
<SlImageComparer>
@@ -63,7 +63,7 @@ Use the `position` attribute to set the initial position of the slider. This is
```
```jsx:react
import { SlImageComparer } from '@shoelace-style/shoelace/dist/react';
import SlImageComparer from '@shoelace-style/shoelace/dist/react/image-comparer';
const App = () => (
<SlImageComparer position={25}>

View File

@@ -14,7 +14,7 @@ The included content will be inserted into the `<sl-include>` element's default
```
```jsx:react
import { SlInclude } from '@shoelace-style/shoelace/dist/react';
import SlInclude from '@shoelace-style/shoelace/dist/react/include';
const App = () => <SlInclude src="https://shoelace.style/assets/examples/include.html" />;
```

View File

@@ -10,7 +10,7 @@ layout: component
```
```jsx:react
import { SlInput } from '@shoelace-style/shoelace/dist/react';
import SlInput from '@shoelace-style/shoelace/dist/react/input';
const App = () => <SlInput />;
```
@@ -30,7 +30,8 @@ Use the `label` attribute to give the input an accessible label. For labels that
```
```jsx:react
import { SlIcon, SlInput } from '@shoelace-style/shoelace/dist/react';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
import SlInput from '@shoelace-style/shoelace/dist/react/input';
const App = () => <SlInput label="What is your name?" />;
```
@@ -44,7 +45,8 @@ Add descriptive help text to an input with the `help-text` attribute. For help t
```
```jsx:react
import { SlIcon, SlInput } from '@shoelace-style/shoelace/dist/react';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
import SlInput from '@shoelace-style/shoelace/dist/react/input';
const App = () => <SlInput label="Nickname" help-text="What would you like people to call you?" />;
```
@@ -58,7 +60,7 @@ Use the `placeholder` attribute to add a placeholder.
```
```jsx:react
import { SlInput } from '@shoelace-style/shoelace/dist/react';
import SlInput from '@shoelace-style/shoelace/dist/react/input';
const App = () => <SlInput placeholder="Type something" />;
```
@@ -72,7 +74,7 @@ Add the `clearable` attribute to add a clear button when the input has content.
```
```jsx:react
import { SlInput } from '@shoelace-style/shoelace/dist/react';
import SlInput from '@shoelace-style/shoelace/dist/react/input';
const App = () => <SlInput placeholder="Clearable" clearable />;
```
@@ -86,7 +88,7 @@ Add the `password-toggle` attribute to add a toggle button that will show the pa
```
```jsx:react
import { SlInput } from '@shoelace-style/shoelace/dist/react';
import SlInput from '@shoelace-style/shoelace/dist/react/input';
const App = () => <SlInput type="password" placeholder="Password Toggle" size="medium" password-toggle />;
```
@@ -100,7 +102,7 @@ Add the `filled` attribute to draw a filled input.
```
```jsx:react
import { SlInput } from '@shoelace-style/shoelace/dist/react';
import SlInput from '@shoelace-style/shoelace/dist/react/input';
const App = () => <SlInput placeholder="Type something" filled />;
```
@@ -114,7 +116,7 @@ Use the `disabled` attribute to disable an input.
```
```jsx:react
import { SlInput } from '@shoelace-style/shoelace/dist/react';
import SlInput from '@shoelace-style/shoelace/dist/react/input';
const App = () => <SlInput placeholder="Disabled" disabled />;
```
@@ -132,7 +134,7 @@ Use the `size` attribute to change an input's size.
```
```jsx:react
import { SlInput } from '@shoelace-style/shoelace/dist/react';
import SlInput from '@shoelace-style/shoelace/dist/react/input';
const App = () => (
<>
@@ -158,7 +160,7 @@ Use the `pill` attribute to give inputs rounded edges.
```
```jsx:react
import { SlInput } from '@shoelace-style/shoelace/dist/react';
import SlInput from '@shoelace-style/shoelace/dist/react/input';
const App = () => (
<>
@@ -184,7 +186,7 @@ The `type` attribute controls the type of input the browser renders.
```
```jsx:react
import { SlInput } from '@shoelace-style/shoelace/dist/react';
import SlInput from '@shoelace-style/shoelace/dist/react/input';
const App = () => (
<>
@@ -219,7 +221,8 @@ Use the `prefix` and `suffix` slots to add icons.
```
```jsx:react
import { SlIcon, SlInput } from '@shoelace-style/shoelace/dist/react';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
import SlInput from '@shoelace-style/shoelace/dist/react/input';
const App = () => (
<>

View File

@@ -28,7 +28,10 @@ layout: component
{% raw %}
```jsx:react
import { SlDivider, SlIcon, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
import SlDivider from '@shoelace-style/shoelace/dist/react/divider';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
const App = () => (
<SlMenu style={{ maxWidth: '200px' }}>
@@ -72,7 +75,8 @@ Add the `disabled` attribute to disable the menu item so it cannot be selected.
{% raw %}
```jsx:react
import { SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
const App = () => (
<SlMenu style={{ maxWidth: '200px' }}>
@@ -114,7 +118,11 @@ Add content to the start and end of menu items using the `prefix` and `suffix` s
{% raw %}
```jsx:react
import { SlBadge, SlDivider, SlIcon, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
import SlBadge from '@shoelace-style/shoelace/dist/react/badge';
import SlDivider from '@shoelace-style/shoelace/dist/react/divider';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
const App = () => (
<SlMenu style={{ maxWidth: '200px' }}>
@@ -160,7 +168,8 @@ Checkbox menu items are visually indistinguishable from regular menu items. Thei
{% raw %}
```jsx:react
import { SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
const App = () => (
<SlMenu style={{ maxWidth: '200px' }}>
@@ -209,7 +218,8 @@ The `value` attribute can be used to assign a hidden value, such as a unique ide
{% raw %}
```jsx:react
import { SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
const App = () => {
function handleSelect(event) {

View File

@@ -22,7 +22,10 @@ layout: component
{% raw %}
```jsx:react
import { SlDivider, SlMenu, SlMenuLabel, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
import SlDivider from '@shoelace-style/shoelace/dist/react/divider';
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
import SlMenuLabel from '@shoelace-style/shoelace/dist/react/menu-label';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
const App = () => (
<SlMenu style={{ maxWidth: '200px' }}>

View File

@@ -22,7 +22,9 @@ You can use [menu items](/components/menu-item), [menu labels](/components/menu-
{% raw %}
```jsx:react
import { SlDivider, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
import SlDivider from '@shoelace-style/shoelace/dist/react/divider';
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
const App = () => (
<SlMenu style={{ maxWidth: '200px' }}>
@@ -42,3 +44,112 @@ const App = () => (
:::tip
Menus are intended for system menus (dropdown menus, select menus, context menus, etc.). They should not be mistaken for navigation menus which serve a different purpose and have a different semantic meaning. If you're building navigation, use `<nav>` and `<a>` elements instead.
:::
## Examples
### In Dropdowns
Menus work really well when used inside [dropdowns](/components/dropdown).
```html:preview
<sl-dropdown>
<sl-button slot="trigger" caret>Edit</sl-button>
<sl-menu>
<sl-menu-item value="cut">Cut</sl-menu-item>
<sl-menu-item value="copy">Copy</sl-menu-item>
<sl-menu-item value="paste">Paste</sl-menu-item>
</sl-menu>
</sl-dropdown>
```
```jsx:react
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown';
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
const App = () => (
<SlDropdown>
<SlButton slot="trigger" caret>Edit</SlButton>
<SlMenu>
<SlMenuItem value="cut">Cut</SlMenuItem>
<SlMenuItem value="copy">Copy</SlMenuItem>
<SlMenuItem value="paste">Paste</SlMenuItem>
</SlMenu>
</SlDropdown>
);
```
### Submenus
To create a submenu, nest an `<sl-menu slot="submenu">` in any [menu item](/components/menu-item).
```html:preview
<sl-menu style="max-width: 200px;">
<sl-menu-item value="undo">Undo</sl-menu-item>
<sl-menu-item value="redo">Redo</sl-menu-item>
<sl-divider></sl-divider>
<sl-menu-item value="cut">Cut</sl-menu-item>
<sl-menu-item value="copy">Copy</sl-menu-item>
<sl-menu-item value="paste">Paste</sl-menu-item>
<sl-divider></sl-divider>
<sl-menu-item>
Find
<sl-menu slot="submenu">
<sl-menu-item value="find">Find…</sl-menu-item>
<sl-menu-item value="find-previous">Find Next</sl-menu-item>
<sl-menu-item value="find-next">Find Previous</sl-menu-item>
</sl-menu>
</sl-menu-item>
<sl-menu-item>
Transformations
<sl-menu slot="submenu">
<sl-menu-item value="uppercase">Make uppercase</sl-menu-item>
<sl-menu-item value="lowercase">Make lowercase</sl-menu-item>
<sl-menu-item value="capitalize">Capitalize</sl-menu-item>
</sl-menu>
</sl-menu-item>
</sl-menu>
```
{% raw %}
```jsx:react
import SlDivider from '@shoelace-style/shoelace/dist/react/divider';
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
const App = () => (
<SlMenu style={{ maxWidth: '200px' }}>
<SlMenuItem value="undo">Undo</SlMenuItem>
<SlMenuItem value="redo">Redo</SlMenuItem>
<SlDivider />
<SlMenuItem value="cut">Cut</SlMenuItem>
<SlMenuItem value="copy">Copy</SlMenuItem>
<SlMenuItem value="paste">Paste</SlMenuItem>
<SlDivider />
<SlMenuItem>
Find
<SlMenu slot="submenu">
<SlMenuItem value="find">Find…</SlMenuItem>
<SlMenuItem value="find-previous">Find Next</SlMenuItem>
<SlMenuItem value="find-next">Find Previous</SlMenuItem>
</SlMenu>
</SlMenuItem>
<SlMenuItem>
Transformations
<SlMenu slot="submenu">
<SlMenuItem value="uppercase">Make uppercase</SlMenuItem>
<SlMenuItem value="lowercase">Make lowercase</SlMenuItem>
<SlMenuItem value="capitalize">Capitalize</SlMenuItem>
</SlMenu>
</SlMenuItem>
</SlMenu>
);
```
:::warning
As a UX best practice, avoid using more than one level of submenus when possible.
:::
{% endraw %}

View File

@@ -45,7 +45,8 @@ The mutation observer will report changes to the content it wraps through the `s
```jsx:react
import { useState } from 'react';
import { SlButton, SlMutationObserver } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlMutationObserver from '@shoelace-style/shoelace/dist/react/mutation-observer';
const css = `
.resize-observer-overview div {
@@ -146,7 +147,8 @@ Use the `child-list` attribute to watch for new child elements that are added or
```jsx:react
import { useState } from 'react';
import { SlButton, SlMutationObserver } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlMutationObserver from '@shoelace-style/shoelace/dist/react/mutation-observer';
const css = `
.mutation-child-list .buttons {

View File

@@ -14,7 +14,8 @@ layout: component
```
```jsx:react
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect>
@@ -40,7 +41,8 @@ Use the `disabled` attribute to disable an option and prevent it from being sele
```
```jsx:react
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect>

View File

@@ -104,7 +104,11 @@ Popup is a low-level utility built specifically for positioning elements. Do not
```jsx:react
import { useState } from 'react';
import { SlPopup, SlSelect, SlMenuItem, SlInput, SlSwitch } from '@shoelace-style/shoelace/dist/react';
import SlPopup from '@shoelace-style/shoelace/dist/react/popup';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
import SlInput from '@shoelace-style/shoelace/dist/react/input';
import SlSwitch from '@shoelace-style/shoelace/dist/react/switch';
const css = `
.popup-overview sl-popup {
@@ -269,7 +273,8 @@ Popups are inactive and hidden until the `active` attribute is applied. Removing
```jsx:react
import { useState } from 'react';
import { SlPopup, SlSwitch } from '@shoelace-style/shoelace/dist/react';
import SlPopup from '@shoelace-style/shoelace/dist/react/popup';
import SlSwitch from '@shoelace-style/shoelace/dist/react/switch';
const css = `
.popup-active span[slot='anchor'] {
@@ -341,7 +346,7 @@ By default, anchors are slotted into the popup using the `anchor` slot. If your
```
```jsx:react
import { SlPopup } from '@shoelace-style/shoelace/dist/react';
import SlPopup from '@shoelace-style/shoelace/dist/react/popup';
const css = `
#external-anchor {
@@ -436,7 +441,9 @@ Since placement is preferred when using `flip`, you can observe the popup's curr
```jsx:react
import { useState } from 'react';
import { SlPopup, SlSelect, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
import SlPopup from '@shoelace-style/shoelace/dist/react/popup';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
const css = `
.popup-placement span[slot='anchor'] {
@@ -538,7 +545,8 @@ Use the `distance` attribute to change the distance between the popup and its an
```jsx:react
import { useState } from 'react';
import { SlPopup, SlRange } from '@shoelace-style/shoelace/dist/react';
import SlPopup from '@shoelace-style/shoelace/dist/react/popup';
import SlRange from '@shoelace-style/shoelace/dist/react/range';
const css = `
.popup-distance span[slot='anchor'] {
@@ -634,7 +642,8 @@ The `skidding` attribute is similar to `distance`, but instead allows you to off
```jsx:react
import { useState } from 'react';
import { SlPopup, SlRange } from '@shoelace-style/shoelace/dist/react';
import SlPopup from '@shoelace-style/shoelace/dist/react/popup';
import SlRange from '@shoelace-style/shoelace/dist/react/range';
const css = `
.popup-skidding span[slot='anchor'] {
@@ -777,7 +786,10 @@ By default, the arrow will be aligned as close to the center of the _anchor_ as
```jsx:react
import { useState } from 'react';
import { SlPopup, SlSelect, SlMenuItem, SlSwitch } from '@shoelace-style/shoelace/dist/react';
import SlPopup from '@shoelace-style/shoelace/dist/react/popup';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
import SlSwitch from '@shoelace-style/shoelace/dist/react/switch';
const css = `
.popup-arrow sl-popup {
@@ -930,7 +942,9 @@ Use the `sync` attribute to make the popup the same width or height as the ancho
```jsx:react
import { useState } from 'react';
import { SlPopup, SlSelect, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
import SlPopup from '@shoelace-style/shoelace/dist/react/popup';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
const css = `
.popup-sync span[slot='anchor'] {
@@ -1041,7 +1055,8 @@ Toggle the switch and scroll the container to see the difference.
```jsx:react
import { useState } from 'react';
import { SlPopup, SlSwitch } from '@shoelace-style/shoelace/dist/react';
import SlPopup from '@shoelace-style/shoelace/dist/react/popup';
import SlSwitch from '@shoelace-style/shoelace/dist/react/switch';
const css = `
.popup-strategy .overflow {
@@ -1149,7 +1164,8 @@ Scroll the container to see how the popup flips to prevent clipping.
```jsx:react
import { useState } from 'react';
import { SlPopup, SlSwitch } from '@shoelace-style/shoelace/dist/react';
import SlPopup from '@shoelace-style/shoelace/dist/react/popup';
import SlSwitch from '@shoelace-style/shoelace/dist/react/switch';
const css = `
.popup-flip .overflow {
@@ -1246,7 +1262,7 @@ Scroll the container to see how the popup changes it's fallback placement to pre
```
```jsx:react
import { SlPopup } from '@shoelace-style/shoelace/dist/react';
import SlPopup from '@shoelace-style/shoelace/dist/react/popup';
const css = `
.popup-flip-fallbacks .overflow {
@@ -1342,7 +1358,8 @@ Toggle the switch to see the difference.
```jsx:react
import { useState } from 'react';
import { SlPopup, SlSwitch } from '@shoelace-style/shoelace/dist/react';
import SlPopup from '@shoelace-style/shoelace/dist/react/popup';
import SlSwitch from '@shoelace-style/shoelace/dist/react/switch';
const css = `
.popup-shift .overflow {
@@ -1454,7 +1471,8 @@ Scroll the container to see the popup resize as its available space changes.
```jsx:react
import { useState } from 'react';
import { SlPopup, SlSwitch } from '@shoelace-style/shoelace/dist/react';
import SlPopup from '@shoelace-style/shoelace/dist/react/popup';
import SlSwitch from '@shoelace-style/shoelace/dist/react/switch';
const css = `
.popup-auto-size .overflow {
@@ -1511,3 +1529,179 @@ const App = () => {
);
};
```
### Virtual Elements
In most cases, popups are anchored to an actual element. Sometimes, it can be useful to anchor them to a non-element. To do this, you can pass a `VirtualElement` to the anchor property. A virtual element must contain a function called `getBoundingClientRect()` that returns a [`DOMRect`](https://developer.mozilla.org/en-US/docs/Web/API/DOMRect) object as shown below.
```ts
const virtualElement = {
getBoundingClientRect() {
// ...
return { width, height, x, y, top, left, right, bottom };
}
};
```
This example anchors a popup to the mouse cursor using a virtual element. As such, a mouse is required to properly view it.
```html:preview
<div class="popup-virtual-element">
<sl-popup placement="right-start">
<div class="circle"></div>
</sl-popup>
<sl-switch>Highlight mouse cursor</sl-switch>
</div>
<script>
const container = document.querySelector('.popup-virtual-element');
const popup = container.querySelector('sl-popup');
const circle = container.querySelector('.circle');
const enabled = container.querySelector('sl-switch');
let clientX = 0;
let clientY = 0;
// Set the virtual element as a property
popup.anchor = {
getBoundingClientRect() {
return {
width: 0,
height: 0,
x: clientX,
y: clientY,
top: clientY,
left: clientX,
right: clientX,
bottom: clientY
};
}
};
// Only activate the popup when the switch is checked
enabled.addEventListener('sl-change', () => {
popup.active = enabled.checked;
});
// Listen for the mouse to move
document.addEventListener('mousemove', handleMouseMove);
// Update the virtual element as the mouse moves
function handleMouseMove(event) {
clientX = event.clientX;
clientY = event.clientY;
// Reposition the popup when the virtual anchor moves
if (popup.active) {
popup.reposition();
}
}
</script>
<style>
/* If you need to set a z-index, set it on the popup part like this */
.popup-virtual-element sl-popup::part(popup) {
z-index: 1000;
pointer-events: none;
}
.popup-virtual-element .circle {
width: 100px;
height: 100px;
border: solid 4px var(--sl-color-primary-600);
border-radius: 50%;
translate: -50px -50px;
animation: 1s virtual-cursor infinite;
}
@keyframes virtual-cursor {
0% { scale: 1; }
50% { scale: 1.1; }
}
</style>
```
```jsx:react
import { useRef, useState } from 'react';
import SlPopup from '@shoelace-style/shoelace/dist/react/popup';
import SlSwitch from '@shoelace-style/shoelace/dist/react/switch';
const css = `
/* If you need to set a z-index, set it on the popup part like this */
.popup-virtual-element sl-popup::part(popup) {
z-index: 1000;
pointer-events: none;
}
.popup-virtual-element .circle {
width: 100px;
height: 100px;
border: solid 4px var(--sl-color-primary-600);
border-radius: 50%;
translate: -50px -50px;
animation: 1s virtual-cursor infinite;
}
@keyframes virtual-cursor {
0% { scale: 1; }
50% { scale: 1.1; }
}
`;
const App = () => {
const [enabled, setEnabled] = useState(false);
const [clientX, setClientX] = useState(0);
const [clientY, setClientY] = useState(0);
const popup = useRef(null);
const circle = useRef(null);
const virtualElement = {
getBoundingClientRect() {
return {
width: 0,
height: 0,
x: clientX,
y: clientY,
top: clientY,
left: clientX,
right: clientX,
bottom: clientY
};
}
};
// Listen for the mouse to move
document.addEventListener('mousemove', handleMouseMove);
// Update the virtual element as the mouse moves
function handleMouseMove(event) {
setClientX(event.clientX);
setClientY(event.clientY);
// Reposition the popup when the virtual anchor moves
if (popup.active) {
popup.current.reposition();
}
}
return (
<>
<div className="popup-virtual-element">
<SlPopup
ref={popup}
placement="right-start"
active={enabled}
anchor={virtualElement}
>
<div ref={circle} className="circle" />
</SlPopup>
<SlSwitch checked={enabled} onSlChange={event => setEnabled(event.target.checked)}>
Highlight mouse cursor
</SlSwitch>
</div>
<style>{css}</style>
</>
);
};
```

View File

@@ -10,7 +10,7 @@ layout: component
```
```jsx:react
import { SlProgressBar } from '@shoelace-style/shoelace/dist/react';
import SlProgressBar from '@shoelace-style/shoelace/dist/react/progress-bar';
const App = () => <SlProgressBar value={50} />;
```
@@ -26,7 +26,7 @@ Use the `label` attribute to label the progress bar and tell assistive devices h
```
```jsx:react
import { SlProgressBar } from '@shoelace-style/shoelace/dist/react';
import SlProgressBar from '@shoelace-style/shoelace/dist/react/progress-bar';
const App = () => <SlProgressBar value="50" label="Upload progress" />;
```
@@ -42,7 +42,7 @@ Use the `--height` custom property to set the progress bar's height.
{% raw %}
```jsx:react
import { SlProgressBar } from '@shoelace-style/shoelace/dist/react';
import SlProgressBar from '@shoelace-style/shoelace/dist/react/progress-bar';
const App = () => <SlProgressBar value={50} style={{ '--height': '6px' }} />;
```
@@ -82,7 +82,9 @@ Use the default slot to show a value.
```jsx:react
import { useState } from 'react';
import { SlButton, SlIcon, SlProgressBar } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
import SlProgressBar from '@shoelace-style/shoelace/dist/react/progress-bar';
const App = () => {
const [value, setValue] = useState(50);
@@ -121,7 +123,7 @@ The `indeterminate` attribute can be used to inform the user that the operation
```
```jsx:react
import { SlProgressBar } from '@shoelace-style/shoelace/dist/react';
import SlProgressBar from '@shoelace-style/shoelace/dist/react/progress-bar';
const App = () => <SlProgressBar indeterminate />;
```

View File

@@ -10,7 +10,7 @@ layout: component
```
```jsx:react
import { SlProgressRing } from '@shoelace-style/shoelace/dist/react';
import SlProgressRing from '@shoelace-style/shoelace/dist/react/progress-ring';
const App = () => <SlProgressRing value="25" />;
```
@@ -28,7 +28,7 @@ Use the `--size` custom property to set the diameter of the progress ring.
{% raw %}
```jsx:react
import { SlProgressRing } from '@shoelace-style/shoelace/dist/react';
import SlProgressRing from '@shoelace-style/shoelace/dist/react/progress-ring';
const App = () => <SlProgressRing value="50" style={{ '--size': '200px' }} />;
```
@@ -46,7 +46,7 @@ Use the `--track-width` and `--indicator-width` custom properties to set the wid
{% raw %}
```jsx:react
import { SlProgressRing } from '@shoelace-style/shoelace/dist/react';
import SlProgressRing from '@shoelace-style/shoelace/dist/react/progress-ring';
const App = () => <SlProgressRing value="50" style={{ '--track-width': '6px', '--indicator-width': '12px' }} />;
```
@@ -70,7 +70,7 @@ To change the color, use the `--track-color` and `--indicator-color` custom prop
{% raw %}
```jsx:react
import { SlProgressRing } from '@shoelace-style/shoelace/dist/react';
import SlProgressRing from '@shoelace-style/shoelace/dist/react/progress-ring';
const App = () => (
<SlProgressRing
@@ -94,7 +94,7 @@ Use the `label` attribute to label the progress ring and tell assistive devices
```
```jsx:react
import { SlProgressRing } from '@shoelace-style/shoelace/dist/react';
import SlProgressRing from '@shoelace-style/shoelace/dist/react/progress-ring';
const App = () => <SlProgressRing value="50" label="Upload progress" />;
```
@@ -134,7 +134,9 @@ Use the default slot to show a label inside the progress ring.
```jsx:react
import { useState } from 'react';
import { SlButton, SlIcon, SlProgressRing } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
import SlProgressRing from '@shoelace-style/shoelace/dist/react/progress-ring';
const App = () => {
const [value, setValue] = useState(50);

View File

@@ -39,7 +39,8 @@ QR codes are useful for providing small pieces of information to users who can q
```jsx:react
import { useState } from 'react';
import { SlQrCode, SlInput } from '@shoelace-style/shoelace/dist/react';
import SlQrCode from '@shoelace-style/shoelace/dist/react/qr-code';
import SlInput from '@shoelace-style/shoelace/dist/react/input';
const css = `
.qr-overview {
@@ -80,7 +81,7 @@ Use the `fill` and `background` attributes to modify the QR code's colors. You s
```
```jsx:react
import { SlQrCode } from '@shoelace-style/shoelace/dist/react';
import SlQrCode from '@shoelace-style/shoelace/dist/react/qr-code';
const App = () => <SlQrCode value="https://shoelace.style/" fill="deeppink" background="white" />;
```
@@ -94,7 +95,7 @@ Use the `size` attribute to change the size of the QR code.
```
```jsx:react
import { SlQrCode } from '@shoelace-style/shoelace/dist/react';
import SlQrCode from '@shoelace-style/shoelace/dist/react/qr-code';
const App = () => <SlQrCode value="https://shoelace.style/" size="64" />;
```
@@ -108,7 +109,7 @@ Create a rounded effect with the `radius` attribute.
```
```jsx:react
import { SlQrCode } from '@shoelace-style/shoelace/dist/react';
import SlQrCode from '@shoelace-style/shoelace/dist/react/qr-code';
const App = () => <SlQrCode value="https://shoelace.style/" radius="0.5" />;
```
@@ -135,7 +136,7 @@ QR codes can be rendered with various levels of [error correction](https://www.q
```
```jsx:react
import { SlQrCode } from '@shoelace-style/shoelace/dist/react';
import SlQrCode from '@shoelace-style/shoelace/dist/react/qr-code';
const css = `
.qr-error-correction {

View File

@@ -16,7 +16,8 @@ Radio buttons are designed to be used with [radio groups](/components/radio-grou
```
```jsx:react
import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
import SlRadioButton from '@shoelace-style/shoelace/dist/react/radio-button';
import SlRadioGroup from '@shoelace-style/shoelace/dist/react/radio-group';
const App = () => (
<SlRadioGroup label="Select an option" name="a" value="1">
@@ -42,7 +43,8 @@ To set the initial value and checked state, use the `value` attribute on the con
```
```jsx:react
import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
import SlRadioButton from '@shoelace-style/shoelace/dist/react/radio-button';
import SlRadioGroup from '@shoelace-style/shoelace/dist/react/radio-group';
const App = () => (
<SlRadioGroup label="Select an option" name="a" value="1">
@@ -66,7 +68,8 @@ Use the `disabled` attribute to disable a radio button.
```
```jsx:react
import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
import SlRadioButton from '@shoelace-style/shoelace/dist/react/radio-button';
import SlRadioGroup from '@shoelace-style/shoelace/dist/react/radio-group';
const App = () => (
<SlRadioGroup label="Select an option" name="a" value="1">
@@ -108,7 +111,8 @@ Use the `size` attribute to change a radio button's size.
```
```jsx:react
import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
import SlRadioButton from '@shoelace-style/shoelace/dist/react/radio-button';
import SlRadioGroup from '@shoelace-style/shoelace/dist/react/radio-group';
const App = () => (
<SlRadioGroup label="Select an option" name="a" value="1">
@@ -164,7 +168,8 @@ Use the `pill` attribute to give radio buttons rounded edges.
```
```jsx:react
import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
import SlRadioButton from '@shoelace-style/shoelace/dist/react/radio-button';
import SlRadioGroup from '@shoelace-style/shoelace/dist/react/radio-group';
const App = () => (
<SlRadioGroup label="Select an option" name="a" value="1">
@@ -216,7 +221,9 @@ Use the `prefix` and `suffix` slots to add icons.
```
```jsx:react
import { SlIcon, SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
import SlRadioButton from '@shoelace-style/shoelace/dist/react/radio-button';
import SlRadioGroup from '@shoelace-style/shoelace/dist/react/radio-group';
const App = () => (
<SlRadioGroup label="Select an option" name="a" value="1">
@@ -268,7 +275,9 @@ You can omit button labels and use icons instead. Make sure to set a `label` att
```
```jsx:react
import { SlIcon, SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
import SlRadioButton from '@shoelace-style/shoelace/dist/react/radio-button';
import SlRadioGroup from '@shoelace-style/shoelace/dist/react/radio-group';
const App = () => (
<SlRadioGroup label="Select an option" name="a" value="neutral">

View File

@@ -14,7 +14,8 @@ layout: component
```
```jsx:react
import { SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
import SlRadio from '@shoelace-style/shoelace/dist/react/radio';
import SlRadioGroup from '@shoelace-style/shoelace/dist/react/radio-group';
const App = () => (
<SlRadioGroup label="Select an option" name="a" value="1">
@@ -40,7 +41,8 @@ Add descriptive help text to a radio group with the `help-text` attribute. For h
```
```jsx:react
import { SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
import SlRadio from '@shoelace-style/shoelace/dist/react/radio';
import SlRadioGroup from '@shoelace-style/shoelace/dist/react/radio-group';
const App = () => (
<SlRadioGroup label="Select an option" help-text="Choose the most appropriate option." name="a" value="1">
@@ -64,7 +66,8 @@ const App = () => (
```
```jsx:react
import { SlRadioButton, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
import SlRadioButton from '@shoelace-style/shoelace/dist/react/radio-button';
import SlRadioGroup from '@shoelace-style/shoelace/dist/react/radio-group';
const App = () => (
<SlRadioGroup label="Select an option" name="a" value="1">
@@ -88,7 +91,8 @@ Radios and radio buttons can be disabled by adding the `disabled` attribute to t
```
```jsx:react
import { SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
import SlRadio from '@shoelace-style/shoelace/dist/react/radio';
import SlRadioGroup from '@shoelace-style/shoelace/dist/react/radio-group';
const App = () => (
<SlRadioGroup label="Select an option" name="a" value="1">
@@ -123,7 +127,8 @@ The size of [Radios](/components/radio) and [Radio Buttons](/components/radio-bu
```jsx react
import { useState } from 'react';
import { SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
import SlRadio from '@shoelace-style/shoelace/dist/react/radio';
import SlRadioGroup from '@shoelace-style/shoelace/dist/react/radio-group';
const App = () => {
const [size, setSize] = useState('medium');
@@ -177,7 +182,10 @@ Setting the `required` attribute to make selecting an option mandatory. If a val
```
```jsx:react
import { SlButton, SlIcon, SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
import SlRadio from '@shoelace-style/shoelace/dist/react/radio';
import SlRadioGroup from '@shoelace-style/shoelace/dist/react/radio-group';
const App = () => {
function handleSubmit(event) {
event.preventDefault();
@@ -247,7 +255,10 @@ Use the `setCustomValidity()` method to set a custom validation message. This wi
```jsx:react
import { useEffect, useRef } from 'react';
import { SlButton, SlIcon, SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
import SlRadio from '@shoelace-style/shoelace/dist/react/radio';
import SlRadioGroup from '@shoelace-style/shoelace/dist/react/radio-group';
const App = () => {
const radioGroup = useRef(null);
const errorMessage = 'You must choose this option';

View File

@@ -16,7 +16,8 @@ Radios are designed to be used with [radio groups](/components/radio-group).
```
```jsx:react
import { SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
import SlRadio from '@shoelace-style/shoelace/dist/react/radio';
import SlRadioGroup from '@shoelace-style/shoelace/dist/react/radio-group';
const App = () => (
<SlRadioGroup label="Select an option" name="a" value="1">
@@ -46,7 +47,8 @@ To set the initial value and checked state, use the `value` attribute on the con
```
```jsx:react
import { SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
import SlRadio from '@shoelace-style/shoelace/dist/react/radio';
import SlRadioGroup from '@shoelace-style/shoelace/dist/react/radio-group';
const App = () => (
<SlRadioGroup label="Select an option" name="a" value="3">
@@ -70,7 +72,8 @@ Use the `disabled` attribute to disable a radio.
```
```jsx:react
import { SlRadio, SlRadioGroup } from '@shoelace-style/shoelace/dist/react';
import SlRadio from '@shoelace-style/shoelace/dist/react/radio';
import SlRadioGroup from '@shoelace-style/shoelace/dist/react/radio-group';
const App = () => (
<SlRadioGroup label="Select an option" name="a" value="1">
@@ -112,7 +115,7 @@ Add the `size` attribute to the [Radio Group](/components/radio-group) to change
```
```jsx react
import { SlRadio } from '@shoelace-style/shoelace/dist/react';
import SlRadio from '@shoelace-style/shoelace/dist/react/radio';
const App = () => (
<>

View File

@@ -10,7 +10,7 @@ layout: component
```
```jsx:react
import { SlRange } from '@shoelace-style/shoelace/dist/react';
import SlRange from '@shoelace-style/shoelace/dist/react/range';
const App = () => <SlRange />;
```
@@ -30,7 +30,7 @@ Use the `label` attribute to give the range an accessible label. For labels that
```
```jsx:react
import { SlRange } from '@shoelace-style/shoelace/dist/react';
import SlRange from '@shoelace-style/shoelace/dist/react/range';
const App = () => <SlRange label="Volume" min={0} max={100} />;
```
@@ -44,7 +44,7 @@ Add descriptive help text to a range with the `help-text` attribute. For help te
```
```jsx:react
import { SlRange } from '@shoelace-style/shoelace/dist/react';
import SlRange from '@shoelace-style/shoelace/dist/react/range';
const App = () => <SlRange label="Volume" help-text="Controls the volume of the current song." min={0} max={100} />;
```
@@ -58,7 +58,7 @@ Use the `min` and `max` attributes to set the range's minimum and maximum values
```
```jsx:react
import { SlRange } from '@shoelace-style/shoelace/dist/react';
import SlRange from '@shoelace-style/shoelace/dist/react/range';
const App = () => <SlRange min={0} max={10} step={1} />;
```
@@ -72,7 +72,7 @@ Use the `disabled` attribute to disable a slider.
```
```jsx:react
import { SlRange } from '@shoelace-style/shoelace/dist/react';
import SlRange from '@shoelace-style/shoelace/dist/react/range';
const App = () => <SlRange disabled />;
```
@@ -86,7 +86,7 @@ By default, the tooltip is shown on top. Set `tooltip` to `bottom` to show it be
```
```jsx:react
import { SlRange } from '@shoelace-style/shoelace/dist/react';
import SlRange from '@shoelace-style/shoelace/dist/react/range';
const App = () => <SlRange tooltip="bottom" />;
```
@@ -100,7 +100,7 @@ To disable the tooltip, set `tooltip` to `none`.
```
```jsx:react
import { SlRange } from '@shoelace-style/shoelace/dist/react';
import SlRange from '@shoelace-style/shoelace/dist/react/range';
const App = () => <SlRange tooltip="none" />;
```
@@ -121,7 +121,7 @@ You can customize the active and inactive portions of the track using the `--tra
{% raw %}
```jsx:react
import { SlRange } from '@shoelace-style/shoelace/dist/react';
import SlRange from '@shoelace-style/shoelace/dist/react/range';
const App = () => (
<SlRange
@@ -154,7 +154,7 @@ You can customize the initial offset of the active track using the `--track-acti
{% raw %}
```jsx:react
import { SlRange } from '@shoelace-style/shoelace/dist/react';
import SlRange from '@shoelace-style/shoelace/dist/react/range';
const App = () => (
<SlRange
@@ -185,7 +185,7 @@ You can change the tooltip's content by setting the `tooltipFormatter` property
```
```jsx:react
import { SlRange } from '@shoelace-style/shoelace/dist/react';
import SlRange from '@shoelace-style/shoelace/dist/react/range';
const App = () => <SlRange min={0} max={100} step={1} tooltipFormatter={value => `Total - ${value}%`} />;
```

View File

@@ -10,7 +10,7 @@ layout: component
```
```jsx:react
import { SlRating } from '@shoelace-style/shoelace/dist/react';
import SlRating from '@shoelace-style/shoelace/dist/react/rating';
const App = () => <SlRating label="Rating" />;
```
@@ -26,7 +26,7 @@ Ratings are commonly identified contextually, so labels aren't displayed. Howeve
```
```jsx:react
import { SlRating } from '@shoelace-style/shoelace/dist/react';
import SlRating from '@shoelace-style/shoelace/dist/react/rating';
const App = () => <SlRating label="Rate this component" />;
```
@@ -40,7 +40,7 @@ Ratings are 0-5 by default. To change the maximum possible value, use the `max`
```
```jsx:react
import { SlRating } from '@shoelace-style/shoelace/dist/react';
import SlRating from '@shoelace-style/shoelace/dist/react/rating';
const App = () => <SlRating label="Rating" max={3} />;
```
@@ -54,7 +54,7 @@ Use the `precision` attribute to let users select fractional ratings.
```
```jsx:react
import { SlRating } from '@shoelace-style/shoelace/dist/react';
import SlRating from '@shoelace-style/shoelace/dist/react/rating';
const App = () => <SlRating label="Rating" precision={0.5} value={2.5} />;
```
@@ -70,7 +70,7 @@ Set the `--symbol-size` custom property to adjust the size.
{% raw %}
```jsx:react
import { SlRating } from '@shoelace-style/shoelace/dist/react';
import SlRating from '@shoelace-style/shoelace/dist/react/rating';
const App = () => <SlRating label="Rating" style={{ '--symbol-size': '2rem' }} />;
```
@@ -86,7 +86,7 @@ Use the `readonly` attribute to display a rating that users can't change.
```
```jsx:react
import { SlRating } from '@shoelace-style/shoelace/dist/react';
import SlRating from '@shoelace-style/shoelace/dist/react/rating';
const App = () => <SlRating label="Rating" readonly value={3} />;
```
@@ -100,7 +100,7 @@ Use the `disable` attribute to disable the rating.
```
```jsx:react
import { SlRating } from '@shoelace-style/shoelace/dist/react';
import SlRating from '@shoelace-style/shoelace/dist/react/rating';
const App = () => <SlRating label="Rating" disabled value={3} />;
```
@@ -152,7 +152,7 @@ The event has a payload with `phase` and `value` properties. The `phase` propert
```jsx:react
import { useState } from 'react';
import { SlRating } from '@shoelace-style/shoelace/dist/react';
import SlRating from '@shoelace-style/shoelace/dist/react/rating';
const terms = ['No rating', 'Terrible', 'Bad', 'OK', 'Good', 'Excellent'];
const css = `
@@ -214,7 +214,7 @@ You can provide custom icons by passing a function to the `getSymbol` property.
{% raw %}
```jsx:react
import { SlRating } from '@shoelace-style/shoelace/dist/react';
import SlRating from '@shoelace-style/shoelace/dist/react/rating';
const App = () => (
<SlRating
@@ -245,7 +245,7 @@ You can also use the `getSymbol` property to render different icons based on val
```
```jsx:react
import { SlRating } from '@shoelace-style/shoelace/dist/react';
import SlRating from '@shoelace-style/shoelace/dist/react/rating';
function getSymbol(value) {
const icons = ['emoji-angry', 'emoji-frown', 'emoji-expressionless', 'emoji-smile', 'emoji-laughing'];

View File

@@ -13,7 +13,7 @@ Localization is handled by the browser's [`Intl.RelativeTimeFormat` API](https:/
```
```jsx:react
import { SlRelativeTime } from '@shoelace-style/shoelace/dist/react';
import SlRelativeTime from '@shoelace-style/shoelace/dist/react/relative-time';
const App = () => <SlRelativeTime date="2020-07-15T09:17:00-04:00" />;
```
@@ -44,7 +44,7 @@ Use the `sync` attribute to update the displayed value automatically as time pas
```
```jsx:react
import { SlRelativeTime } from '@shoelace-style/shoelace/dist/react';
import SlRelativeTime from '@shoelace-style/shoelace/dist/react/relative-time';
const date = new Date(new Date().getTime() - 60000);
@@ -62,7 +62,7 @@ You can change how the time is displayed using the `format` attribute. Note that
```
```jsx:react
import { SlRelativeTime } from '@shoelace-style/shoelace/dist/react';
import SlRelativeTime from '@shoelace-style/shoelace/dist/react/relative-time';
const App = () => (
<>
@@ -88,7 +88,7 @@ Russian: <sl-relative-time date="2020-07-15T09:17:00-04:00" lang="ru"></sl-relat
```
```jsx:react
import { SlRelativeTime } from '@shoelace-style/shoelace/dist/react';
import SlRelativeTime from '@shoelace-style/shoelace/dist/react/relative-time';
const App = () => (
<>

View File

@@ -36,7 +36,7 @@ The resize observer will report changes to the dimensions of the elements it wra
```
```jsx:react
import { SlResizeObserver } from '@shoelace-style/shoelace/dist/react';
import SlResizeObserver from '@shoelace-style/shoelace/dist/react/resize-observer';
const css = `
.resize-observer-overview div {

View File

@@ -17,7 +17,8 @@ layout: component
```
```jsx:react
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect>
@@ -50,7 +51,8 @@ Use the `label` attribute to give the select an accessible label. For labels tha
```
```jsx:react
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect label="Select one">
@@ -74,7 +76,8 @@ Add descriptive help text to a select with the `help-text` attribute. For help t
```
```jsx:react
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect label="Experience" help-text="Please tell us your skill level.">
@@ -98,7 +101,8 @@ Use the `placeholder` attribute to add a placeholder.
```
```jsx:react
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect placeholder="Select one">
@@ -122,7 +126,8 @@ Use the `clearable` attribute to make the control clearable. The clear button on
```
```jsx:react
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect placeholder="Clearable" clearable>
@@ -146,7 +151,8 @@ Add the `filled` attribute to draw a filled select.
```
```jsx:react
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect filled>
@@ -170,7 +176,8 @@ Use the `pill` attribute to give selects rounded edges.
```
```jsx:react
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect pill>
@@ -194,7 +201,8 @@ Use the `disabled` attribute to disable a select.
```
```jsx:react
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect placeholder="Disabled" disabled>
@@ -221,7 +229,8 @@ To allow multiple options to be selected, use the `multiple` attribute. It's a g
```
```jsx:react
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect label="Select a Few" value="option-1 option-2 option-3" multiple clearable>
@@ -241,7 +250,9 @@ Note that multi-select options may wrap, causing the control to expand verticall
### Setting Initial Values
Use the `value` attribute to set the initial selection. When using `multiple`, use space-delimited values to select more than one option.
Use the `value` attribute to set the initial selection.
When using `multiple`, the `value` _attribute_ uses space-delimited values to select more than one option. Because of this, `<sl-option>` values cannot contain spaces. If you're accessing the `value` _property_ through Javascript, it will be an array.
```html:preview
<sl-select value="option-1 option-2" multiple clearable>
@@ -253,7 +264,9 @@ Use the `value` attribute to set the initial selection. When using `multiple`, u
```
```jsx:react
import { SlDivider, SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
import SlDivider from '@shoelace-style/shoelace/dist/react/divider';
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect value="option-1 option-2" multiple clearable>
@@ -283,7 +296,8 @@ Use `<sl-divider>` to group listbox items visually. You can also use `<small>` t
```
```jsx:react
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect>
@@ -326,7 +340,8 @@ Use the `size` attribute to change a select's size. Note that size does not appl
```
```jsx:react
import { SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<>
@@ -368,10 +383,8 @@ The preferred placement of the select's listbox can be set with the `placement`
```
```jsx:react
import {
SlOption,
SlSelect
} from '@shoelace-style/shoelace/dist/react';
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect placement="top">
@@ -410,7 +423,9 @@ Use the `prefix` slot to prepend an icon to the control.
```
```jsx:react
import { SlIcon, SlOption, SlSelect } from '@shoelace-style/shoelace/dist/react';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<>
@@ -437,3 +452,53 @@ const App = () => (
</>
);
```
### Custom Tags
When multiple options can be selected, you can provide custom tags by passing a function to the `getTag` property. Your function can return a string of HTML, a <a href="https://lit.dev/docs/templates/overview/">Lit Template</a>, or an [`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement). The `getTag()` function will be called for each option. The first argument is an `<sl-option>` element and the second argument is the tag's index (its position in the tag list).
Remember that custom tags are rendered in a shadow root. To style them, you can use the `style` attribute in your template or you can add your own [parts](/getting-started/customizing/#css-parts) and target them with the [`::part()`](https://developer.mozilla.org/en-US/docs/Web/CSS/::part) selector.
```html:preview
<sl-select
placeholder="Select one"
value="email phone"
multiple
clearable
class="custom-tag"
>
<sl-option value="email">
<sl-icon slot="prefix" name="envelope"></sl-icon>
Email
</sl-option>
<sl-option value="phone">
<sl-icon slot="prefix" name="telephone"></sl-icon>
Phone
</sl-option>
<sl-option value="chat">
<sl-icon slot="prefix" name="chat-dots"></sl-icon>
Chat
</sl-option>
</sl-select>
<script type="module">
const select = document.querySelector('.custom-tag');
select.getTag = (option, index) => {
// Use the same icon used in the <sl-option>
const name = option.querySelector('sl-icon[slot="prefix"]').name;
// You can return a string, a Lit Template, or an HTMLElement here
return `
<sl-tag removable>
<sl-icon name="${name}" style="padding-inline-end: .5rem;"></sl-icon>
${option.getTextLabel()}
</sl-tag>
`;
};
</script>
```
:::warning
Be sure you trust the content you are outputting! Passing unsanitized user input to `getTag()` can result in XSS vulnerabilities.
:::

View File

@@ -56,7 +56,7 @@ Skeletons try not to be opinionated, as there are endless possibilities for desi
```
```jsx:react
import { SlSkeleton } from '@shoelace-style/shoelace/dist/react';
import SlSkeleton from '@shoelace-style/shoelace/dist/react/skeleton';
const css = `
.skeleton-overview header {
@@ -139,7 +139,7 @@ There are two built-in effects, `sheen` and `pulse`. Effects are intentionally s
```
```jsx:react
import { SlSkeleton } from '@shoelace-style/shoelace/dist/react';
import SlSkeleton from '@shoelace-style/shoelace/dist/react/skeleton';
const css = `
.skeleton-effects {
@@ -200,7 +200,7 @@ Use multiple skeletons and some clever styles to simulate paragraphs.
```
```jsx:react
import { SlSkeleton } from '@shoelace-style/shoelace/dist/react';
import SlSkeleton from '@shoelace-style/shoelace/dist/react/skeleton';
const css = `
.skeleton-paragraphs sl-skeleton {
@@ -265,7 +265,7 @@ Set a matching width and height to make a circle, square, or rounded avatar skel
```
```jsx:react
import { SlSkeleton } from '@shoelace-style/shoelace/dist/react';
import SlSkeleton from '@shoelace-style/shoelace/dist/react/skeleton';
const css = `
.skeleton-avatars sl-skeleton {
@@ -360,7 +360,7 @@ Use the `--border-radius` custom property to make circles, squares, and rectangl
```
```jsx:react
import { SlSkeleton } from '@shoelace-style/shoelace/dist/react';
import SlSkeleton from '@shoelace-style/shoelace/dist/react/skeleton';
const css = `
.skeleton-shapes sl-skeleton {
@@ -423,7 +423,7 @@ Set the `--color` and `--sheen-color` custom properties to adjust the skeleton's
{% raw %}
```jsx:react
import { SlSkeleton } from '@shoelace-style/shoelace/dist/react';
import SlSkeleton from '@shoelace-style/shoelace/dist/react/skeleton';
const css = `
.skeleton-avatars sl-skeleton {

View File

@@ -10,7 +10,7 @@ layout: component
```
```jsx:react
import { SlSpinner } from '@shoelace-style/shoelace/dist/react';
import SlSpinner from '@shoelace-style/shoelace/dist/react/spinner';
const App = () => <SlSpinner />;
```
@@ -30,7 +30,7 @@ Spinners are sized based on the current font size. To change their size, set the
{% raw %}
```jsx:react
import { SlSpinner } from '@shoelace-style/shoelace/dist/react';
import SlSpinner from '@shoelace-style/shoelace/dist/react/spinner';
const App = () => (
<>
@@ -54,7 +54,7 @@ The width of the spinner's track can be changed by setting the `--track-width` c
{% raw %}
```jsx:react
import { SlSpinner } from '@shoelace-style/shoelace/dist/react';
import SlSpinner from '@shoelace-style/shoelace/dist/react/spinner';
const App = () => (
<SlSpinner
@@ -79,7 +79,7 @@ The spinner's colors can be changed by setting the `--indicator-color` and `--tr
{% raw %}
```jsx:react
import { SlSpinner } from '@shoelace-style/shoelace/dist/react';
import SlSpinner from '@shoelace-style/shoelace/dist/react/spinner';
const App = () => (
<SlSpinner

View File

@@ -9,13 +9,13 @@ layout: component
<sl-split-panel>
<div
slot="start"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center;"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
>
Start
</div>
<div
slot="end"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center;"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
>
End
</div>
@@ -25,7 +25,7 @@ layout: component
{% raw %}
```jsx:react
import { SlSplitPanel } from '@shoelace-style/shoelace/dist/react';
import SlSplitPanel from '@shoelace-style/shoelace/dist/react/split-panel';
const App = () => (
<SlSplitPanel>
@@ -69,13 +69,13 @@ To set the initial position, use the `position` attribute. If no position is pro
<sl-split-panel position="75">
<div
slot="start"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center;"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
>
Start
</div>
<div
slot="end"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center;"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
>
End
</div>
@@ -90,13 +90,13 @@ To set the initial position in pixels instead of a percentage, use the `position
<sl-split-panel position-in-pixels="150">
<div
slot="start"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center;"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
>
Start
</div>
<div
slot="end"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center;"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
>
End
</div>
@@ -106,7 +106,7 @@ To set the initial position in pixels instead of a percentage, use the `position
{% raw %}
```jsx:react
import { SlSplitPanel } from '@shoelace-style/shoelace/dist/react';
import SlSplitPanel from '@shoelace-style/shoelace/dist/react/split-panel';
const App = () => (
<SlSplitPanel position="200">
@@ -148,13 +148,13 @@ Add the `vertical` attribute to render the split panel in a vertical orientation
<sl-split-panel vertical style="height: 400px;">
<div
slot="start"
style="height: 100%; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center;"
style="height: 100%; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
>
Start
</div>
<div
slot="end"
style="height: 100%; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center;"
style="height: 100%; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
>
End
</div>
@@ -164,7 +164,7 @@ Add the `vertical` attribute to render the split panel in a vertical orientation
{% raw %}
```jsx:react
import { SlSplitPanel } from '@shoelace-style/shoelace/dist/react';
import SlSplitPanel from '@shoelace-style/shoelace/dist/react/split-panel';
const App = () => (
<SlSplitPanel vertical style={{ height: '400px' }}>
@@ -207,13 +207,13 @@ To snap panels at specific positions while dragging, add the `snap` attribute wi
<sl-split-panel snap="100px 50%">
<div
slot="start"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center;"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
>
Start
</div>
<div
slot="end"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center;"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
>
End
</div>
@@ -252,7 +252,7 @@ To snap panels at specific positions while dragging, add the `snap` attribute wi
{% raw %}
```jsx:react
import { SlSplitPanel } from '@shoelace-style/shoelace/dist/react';
import SlSplitPanel from '@shoelace-style/shoelace/dist/react/split-panel';
const css = `
.split-panel-snapping {
@@ -328,13 +328,13 @@ Add the `disabled` attribute to prevent the divider from being repositioned.
<sl-split-panel disabled>
<div
slot="start"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center;"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
>
Start
</div>
<div
slot="end"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center;"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
>
End
</div>
@@ -344,7 +344,7 @@ Add the `disabled` attribute to prevent the divider from being repositioned.
{% raw %}
```jsx:react
import { SlSplitPanel } from '@shoelace-style/shoelace/dist/react';
import SlSplitPanel from '@shoelace-style/shoelace/dist/react/split-panel';
const App = () => (
<SlSplitPanel disabled>
@@ -389,13 +389,13 @@ Try resizing the example below with each option and notice how the panels respon
<sl-split-panel>
<div
slot="start"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center;"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
>
Start
</div>
<div
slot="end"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center;"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
>
End
</div>
@@ -421,7 +421,9 @@ Try resizing the example below with each option and notice how the panels respon
```jsx:react
import { useState } from 'react';
import { SlSplitPanel, SlSelect, SlMenuItem } from '@shoelace-style/shoelace/dist/react';
import SlSplitPanel from '@shoelace-style/shoelace/dist/react/split-panel';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
const App = () => {
const [primary, setPrimary] = useState('');
@@ -482,13 +484,13 @@ This examples demonstrates how you can ensure both panels are at least 150px usi
<sl-split-panel style="--min: 150px; --max: calc(100% - 150px);">
<div
slot="start"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center;"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
>
Start
</div>
<div
slot="end"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center;"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
>
End
</div>
@@ -498,7 +500,7 @@ This examples demonstrates how you can ensure both panels are at least 150px usi
{% raw %}
```jsx:react
import { SlSplitPanel } from '@shoelace-style/shoelace/dist/react';
import SlSplitPanel from '@shoelace-style/shoelace/dist/react/split-panel';
const App = () => (
<SlSplitPanel style={{ '--min': '150px', '--max': 'calc(100% - 150px)' }}>
@@ -540,7 +542,7 @@ Create complex layouts that can be repositioned independently by nesting split p
<sl-split-panel>
<div
slot="start"
style="height: 400px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center;"
style="height: 400px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden"
>
Start
</div>
@@ -548,13 +550,13 @@ Create complex layouts that can be repositioned independently by nesting split p
<sl-split-panel vertical style="height: 400px;">
<div
slot="start"
style="height: 100%; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center;"
style="height: 100%; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden"
>
Top
</div>
<div
slot="end"
style="height: 100%; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center;"
style="height: 100%; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden"
>
Bottom
</div>
@@ -566,7 +568,7 @@ Create complex layouts that can be repositioned independently by nesting split p
{% raw %}
```jsx:react
import { SlSplitPanel } from '@shoelace-style/shoelace/dist/react';
import SlSplitPanel from '@shoelace-style/shoelace/dist/react/split-panel';
const App = () => (
<SlSplitPanel>
@@ -625,13 +627,13 @@ You can target the `divider` part to apply CSS properties to the divider. To add
<sl-icon slot="divider" name="grip-vertical"></sl-icon>
<div
slot="start"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center;"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
>
Start
</div>
<div
slot="end"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center;"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
>
End
</div>
@@ -641,7 +643,8 @@ You can target the `divider` part to apply CSS properties to the divider. To add
{% raw %}
```jsx:react
import { SlSplitPanel, SlIcon } from '@shoelace-style/shoelace/dist/react';
import SlSplitPanel from '@shoelace-style/shoelace/dist/react/split-panel';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
const App = () => (
<SlSplitPanel style={{ '--divider-width': '20px' }}>
@@ -684,13 +687,13 @@ Here's a more elaborate example that changes the divider's color and width and a
<sl-icon slot="divider" name="grip-vertical"></sl-icon>
<div
slot="start"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center;"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
>
Start
</div>
<div
slot="end"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center;"
style="height: 200px; background: var(--sl-color-neutral-50); display: flex; align-items: center; justify-content: center; overflow: hidden;"
>
End
</div>
@@ -728,7 +731,8 @@ Here's a more elaborate example that changes the divider's color and width and a
{% raw %}
```jsx:react
import { SlSplitPanel, SlIcon } from '@shoelace-style/shoelace/dist/react';
import SlSplitPanel from '@shoelace-style/shoelace/dist/react/split-panel';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
const css = `
.split-panel-divider sl-split-panel {

View File

@@ -10,7 +10,7 @@ layout: component
```
```jsx:react
import { SlSwitch } from '@shoelace-style/shoelace/dist/react';
import SlSwitch from '@shoelace-style/shoelace/dist/react/switch';
const App = () => <SlSwitch>Switch</SlSwitch>;
```
@@ -30,7 +30,7 @@ Use the `checked` attribute to activate the switch.
```
```jsx:react
import { SlSwitch } from '@shoelace-style/shoelace/dist/react';
import SlSwitch from '@shoelace-style/shoelace/dist/react/switch';
const App = () => <SlSwitch checked>Checked</SlSwitch>;
```
@@ -44,7 +44,7 @@ Use the `disabled` attribute to disable the switch.
```
```jsx:react
import { SlSwitch } from '@shoelace-style/shoelace/dist/react';
import SlSwitch from '@shoelace-style/shoelace/dist/react/switch';
const App = () => <SlSwitch disabled>Disabled</SlSwitch>;
```
@@ -62,7 +62,7 @@ Use the `size` attribute to change a switch's size.
```
```jsx:react
import { SlSwitch } from '@shoelace-style/shoelace/dist/react';
import SlSwitch from '@shoelace-style/shoelace/dist/react/switch';
const App = () => (
<>
@@ -86,7 +86,7 @@ Use the available custom properties to change how the switch is styled.
{% raw %}
```jsx:react
import { SlSwitch } from '@shoelace-style/shoelace/dist/react';
import SlSwitch from '@shoelace-style/shoelace/dist/react/switch';
const App = () => (
<SlSwitch

View File

@@ -22,7 +22,9 @@ Tab groups make use of [tabs](/components/tab) and [tab panels](/components/tab-
```
```jsx:react
import { SlTab, SlTabGroup, SlTabPanel } from '@shoelace-style/shoelace/dist/react';
import SlTab from '@shoelace-style/shoelace/dist/react/tab';
import SlTabGroup from '@shoelace-style/shoelace/dist/react/tab-group';
import SlTabPanel from '@shoelace-style/shoelace/dist/react/tab-panel';
const App = () => (
<SlTabGroup>
@@ -68,7 +70,9 @@ Tabs can be shown on the bottom by setting `placement` to `bottom`.
```
```jsx:react
import { SlTab, SlTabGroup, SlTabPanel } from '@shoelace-style/shoelace/dist/react';
import SlTab from '@shoelace-style/shoelace/dist/react/tab';
import SlTabGroup from '@shoelace-style/shoelace/dist/react/tab-group';
import SlTabPanel from '@shoelace-style/shoelace/dist/react/tab-panel';
const App = () => (
<SlTabGroup placement="bottom">
@@ -112,7 +116,9 @@ Tabs can be shown on the starting side by setting `placement` to `start`.
```
```jsx:react
import { SlTab, SlTabGroup, SlTabPanel } from '@shoelace-style/shoelace/dist/react';
import SlTab from '@shoelace-style/shoelace/dist/react/tab';
import SlTabGroup from '@shoelace-style/shoelace/dist/react/tab-group';
import SlTabPanel from '@shoelace-style/shoelace/dist/react/tab-panel';
const App = () => (
<SlTabGroup placement="start">
@@ -156,7 +162,9 @@ Tabs can be shown on the ending side by setting `placement` to `end`.
```
```jsx:react
import { SlTab, SlTabGroup, SlTabPanel } from '@shoelace-style/shoelace/dist/react';
import SlTab from '@shoelace-style/shoelace/dist/react/tab';
import SlTabGroup from '@shoelace-style/shoelace/dist/react/tab-group';
import SlTabPanel from '@shoelace-style/shoelace/dist/react/tab-panel';
const App = () => (
<SlTabGroup placement="end">
@@ -218,7 +226,9 @@ Add the `closable` attribute to a tab to show a close button. This example shows
```
```jsx:react
import { SlTab, SlTabGroup, SlTabPanel } from '@shoelace-style/shoelace/dist/react';
import SlTab from '@shoelace-style/shoelace/dist/react/tab';
import SlTabGroup from '@shoelace-style/shoelace/dist/react/tab-group';
import SlTabPanel from '@shoelace-style/shoelace/dist/react/tab-panel';
const App = () => {
function handleClose(event) {
@@ -310,7 +320,9 @@ When there are more tabs than horizontal space allows, the nav will be scrollabl
```
```jsx:react
import { SlTab, SlTabGroup, SlTabPanel } from '@shoelace-style/shoelace/dist/react';
import SlTab from '@shoelace-style/shoelace/dist/react/tab';
import SlTabGroup from '@shoelace-style/shoelace/dist/react/tab-group';
import SlTabPanel from '@shoelace-style/shoelace/dist/react/tab-panel';
const App = () => (
<SlTabGroup>
@@ -418,7 +430,9 @@ When focused, keyboard users can press [[Left]] or [[Right]] to select the desir
```
```jsx:react
import { SlTab, SlTabGroup, SlTabPanel } from '@shoelace-style/shoelace/dist/react';
import SlTab from '@shoelace-style/shoelace/dist/react/tab';
import SlTabGroup from '@shoelace-style/shoelace/dist/react/tab-group';
import SlTabPanel from '@shoelace-style/shoelace/dist/react/tab-panel';
const App = () => (
<SlTabGroup activation="manual">

View File

@@ -20,7 +20,9 @@ layout: component
```
```jsx:react
import { SlTab, SlTabGroup, SlTabPanel } from '@shoelace-style/shoelace/dist/react';
import SlTab from '@shoelace-style/shoelace/dist/react/tab';
import SlTabGroup from '@shoelace-style/shoelace/dist/react/tab-group';
import SlTabPanel from '@shoelace-style/shoelace/dist/react/tab-panel';
const App = () => (
<SlTabGroup>

View File

@@ -13,7 +13,7 @@ layout: component
```
```jsx:react
import { SlTab } from '@shoelace-style/shoelace/dist/react';
import SlTab from '@shoelace-style/shoelace/dist/react/tab';
const App = () => (
<>

View File

@@ -14,7 +14,7 @@ layout: component
```
```jsx:react
import { SlTag } from '@shoelace-style/shoelace/dist/react';
import SlTag from '@shoelace-style/shoelace/dist/react/tag';
const App = () => (
<>
@@ -40,7 +40,7 @@ Use the `size` attribute to change a tab's size.
```
```jsx:react
import { SlTag } from '@shoelace-style/shoelace/dist/react';
import SlTag from '@shoelace-style/shoelace/dist/react/tag';
const App = () => (
<>
@@ -62,7 +62,7 @@ Use the `pill` attribute to give tabs rounded edges.
```
```jsx:react
import { SlTag } from '@shoelace-style/shoelace/dist/react';
import SlTag from '@shoelace-style/shoelace/dist/react/tag';
const App = () => (
<>
@@ -108,7 +108,7 @@ Use the `removable` attribute to add a remove button to the tag.
```
```jsx:react
import { SlTag } from '@shoelace-style/shoelace/dist/react';
import SlTag from '@shoelace-style/shoelace/dist/react/tag';
const css = `
.tags-removable sl-tag {

View File

@@ -10,7 +10,7 @@ layout: component
```
```jsx:react
import { SlTextarea } from '@shoelace-style/shoelace/dist/react';
import SlTextarea from '@shoelace-style/shoelace/dist/react/textarea';
const App = () => <SlTextarea />;
```
@@ -30,7 +30,7 @@ Use the `label` attribute to give the textarea an accessible label. For labels t
```
```jsx:react
import { SlTextarea } from '@shoelace-style/shoelace/dist/react';
import SlTextarea from '@shoelace-style/shoelace/dist/react/textarea';
const App = () => <SlTextarea label="Comments" />;
```
@@ -44,7 +44,7 @@ Add descriptive help text to a textarea with the `help-text` attribute. For help
```
```jsx:react
import { SlTextarea } from '@shoelace-style/shoelace/dist/react';
import SlTextarea from '@shoelace-style/shoelace/dist/react/textarea';
const App = () => <SlTextarea label="Feedback" help-text="Please tell us what you think." />;
```
@@ -58,7 +58,7 @@ Use the `rows` attribute to change the number of text rows that get shown.
```
```jsx:react
import { SlTextarea } from '@shoelace-style/shoelace/dist/react';
import SlTextarea from '@shoelace-style/shoelace/dist/react/textarea';
const App = () => <SlTextarea rows={2} />;
```
@@ -72,7 +72,7 @@ Use the `placeholder` attribute to add a placeholder.
```
```jsx:react
import { SlTextarea } from '@shoelace-style/shoelace/dist/react';
import SlTextarea from '@shoelace-style/shoelace/dist/react/textarea';
const App = () => <SlTextarea placeholder="Type something" />;
```
@@ -86,7 +86,7 @@ Add the `filled` attribute to draw a filled textarea.
```
```jsx:react
import { SlTextarea } from '@shoelace-style/shoelace/dist/react';
import SlTextarea from '@shoelace-style/shoelace/dist/react/textarea';
const App = () => <SlTextarea placeholder="Type something" filled />;
```
@@ -100,7 +100,7 @@ Use the `disabled` attribute to disable a textarea.
```
```jsx:react
import { SlTextarea } from '@shoelace-style/shoelace/dist/react';
import SlTextarea from '@shoelace-style/shoelace/dist/react/textarea';
const App = () => <SlTextarea placeholder="Textarea" disabled />;
```
@@ -118,7 +118,7 @@ Use the `size` attribute to change a textarea's size.
```
```jsx:react
import { SlTextarea } from '@shoelace-style/shoelace/dist/react';
import SlTextarea from '@shoelace-style/shoelace/dist/react/textarea';
const App = () => (
<>
@@ -140,7 +140,7 @@ By default, textareas can be resized vertically by the user. To prevent resizing
```
```jsx:react
import { SlTextarea } from '@shoelace-style/shoelace/dist/react';
import SlTextarea from '@shoelace-style/shoelace/dist/react/textarea';
const App = () => <SlTextarea resize="none" />;
```
@@ -154,7 +154,7 @@ Textareas will automatically resize to expand to fit their content when `resize`
```
```jsx:react
import { SlTextarea } from '@shoelace-style/shoelace/dist/react';
import SlTextarea from '@shoelace-style/shoelace/dist/react/textarea';
const App = () => <SlTextarea resize="auto" />;
```

View File

@@ -16,7 +16,8 @@ Tooltips use `display: contents` so they won't interfere with how elements are p
```
```jsx:react
import { SlButton, SlTooltip } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlTooltip from '@shoelace-style/shoelace/dist/react/tooltip';
const App = () => (
<SlTooltip content="This is a tooltip">
@@ -125,7 +126,8 @@ Use the `placement` attribute to set the preferred placement of the tooltip.
```
```jsx:react
import { SlButton, SlTooltip } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlTooltip from '@shoelace-style/shoelace/dist/react/tooltip';
const css = `
.tooltip-placement-example {
@@ -235,7 +237,8 @@ Set the `trigger` attribute to `click` to toggle the tooltip on click instead of
```
```jsx:react
import { SlButton, SlTooltip } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlTooltip from '@shoelace-style/shoelace/dist/react/tooltip';
const App = () => (
<SlTooltip content="Click again to dismiss" trigger="click">
@@ -267,7 +270,9 @@ Tooltips can be controller programmatically by setting the `trigger` attribute t
```jsx:react
import { useState } from 'react';
import { SlAvatar, SlButton, SlTooltip } from '@shoelace-style/shoelace/dist/react';
import SlAvatar from '@shoelace-style/shoelace/dist/react/avatar';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlTooltip from '@shoelace-style/shoelace/dist/react/tooltip';
const App = () => {
const [open, setOpen] = useState(false);
@@ -301,7 +306,8 @@ You can control the size of tooltip arrows by overriding the `--sl-tooltip-arrow
{% raw %}
```jsx:react
import { SlButton, SlTooltip } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlTooltip from '@shoelace-style/shoelace/dist/react/tooltip';
const App = () => (
<div style={{ '--sl-tooltip-arrow-size': '0' }}>
@@ -339,7 +345,8 @@ Use the `content` slot to create tooltips with HTML content. Tooltips are design
```
```jsx:react
import { SlButton, SlTooltip } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlTooltip from '@shoelace-style/shoelace/dist/react/tooltip';
const App = () => (
<SlTooltip>
@@ -365,7 +372,8 @@ Use the `--max-width` custom property to change the width the tooltip can grow t
{% raw %}
```jsx:react
import { SlButton, SlTooltip } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlTooltip from '@shoelace-style/shoelace/dist/react/tooltip';
const App = () => (
<SlTooltip style={{ '--max-width': '80px' }} content="This tooltip will wrap after only 80 pixels.">
@@ -402,7 +410,8 @@ Tooltips will be clipped if they're inside a container that has `overflow: auto|
```
```jsx:react
import { SlButton, SlTooltip } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlTooltip from '@shoelace-style/shoelace/dist/react/tooltip';
const css = `
.tooltip-hoist {

View File

@@ -20,7 +20,8 @@ layout: component
<!-- prettier-ignore -->
```jsx:react
import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react';
import SlTree from '@shoelace-style/shoelace/dist/react/tree';
import SlTreeItem from '@shoelace-style/shoelace/dist/react/tree-item';
const App = () => (
<SlTree>
@@ -62,7 +63,8 @@ A tree item can contain other tree items. This allows the node to be expanded or
<!-- prettier-ignore -->
```jsx:react
import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react';
import SlTree from '@shoelace-style/shoelace/dist/react/tree';
import SlTreeItem from '@shoelace-style/shoelace/dist/react/tree-item';
const App = () => (
<SlTree>
@@ -102,7 +104,8 @@ Use the `selected` attribute to select a tree item initially.
<!-- prettier-ignore -->
```jsx:react
import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react';
import SlTree from '@shoelace-style/shoelace/dist/react/tree';
import SlTreeItem from '@shoelace-style/shoelace/dist/react/tree-item';
const App = () => (
<SlTree>
@@ -142,7 +145,8 @@ Use the `expanded` attribute to expand a tree item initially.
<!-- prettier-ignore -->
```jsx:react
import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react';
import SlTree from '@shoelace-style/shoelace/dist/react/tree';
import SlTreeItem from '@shoelace-style/shoelace/dist/react/tree-item';
const App = () => (
<SlTree>

View File

@@ -37,7 +37,8 @@ layout: component
<!-- prettier-ignore -->
```jsx:react
import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react';
import SlTree from '@shoelace-style/shoelace/dist/react/tree';
import SlTreeItem from '@shoelace-style/shoelace/dist/react/tree-item';
const App = () => (
<SlTree>
@@ -118,7 +119,8 @@ The `selection` attribute lets you change the selection behavior of the tree.
<!-- prettier-ignore -->
```jsx:react
import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react';
import SlTree from '@shoelace-style/shoelace/dist/react/tree';
import SlTreeItem from '@shoelace-style/shoelace/dist/react/tree-item';
const App = () => {
const [selection, setSelection] = useState('single');
@@ -197,7 +199,8 @@ Indent guides can be drawn by setting `--indent-guide-width`. You can also chang
<!-- prettier-ignore -->
```jsx:react
import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react';
import SlTree from '@shoelace-style/shoelace/dist/react/tree';
import SlTreeItem from '@shoelace-style/shoelace/dist/react/tree-item';
const App = () => (
<SlTree class="tree-with-lines" style={{ '--indent-guide-width': '1px' }}>
@@ -265,7 +268,8 @@ If you want to disable this behavior after the first load, simply remove the `la
```
```jsx:react
import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react';
import SlTree from '@shoelace-style/shoelace/dist/react/tree';
import SlTreeItem from '@shoelace-style/shoelace/dist/react/tree-item';
const App = () => {
const [childItems, setChildItems] = useState([]);
@@ -340,7 +344,8 @@ Use the `expand-icon` and `collapse-icon` slots to change the expand and collaps
<!-- prettier-ignore -->
```jsx:react
import { SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react';
import SlTree from '@shoelace-style/shoelace/dist/react/tree';
import SlTreeItem from '@shoelace-style/shoelace/dist/react/tree-item';
const App = () => (
<SlTree>
@@ -424,7 +429,9 @@ Decorative icons can be used before labels to provide hints for each node.
```
```jsx:react
import { SlIcon, SlTree, SlTreeItem } from '@shoelace-style/shoelace/dist/react';
import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
import SlTree from '@shoelace-style/shoelace/dist/react/tree';
import SlTreeItem from '@shoelace-style/shoelace/dist/react/tree-item';
const App = () => {
return (

View File

@@ -39,13 +39,28 @@ Now you can start using components!
Every Shoelace component is available to import as a React component. Note that we're importing the `<SlButton>` _React component_ instead of the `<sl-button>` _custom element_ in the example below.
```jsx
import { SlButton } from '@shoelace-style/shoelace/%NPMDIR%/react';
import SlButton from '@shoelace-style/shoelace/%NPMDIR%/react/button';
const MyComponent = () => <SlButton variant="primary">Click me</SlButton>;
export default MyComponent;
```
#### Notes about tree shaking
Previously, it was recommended to import from a single entrypoint like so:
```jsx
import { SlButton } from '@shoelace-style/shoelace/%NPMDIR%/react';
```
However, tree-shaking extra Shoelace components proved to be a challenge. As a result, we now recommend cherry-picking components you want to use, rather than importing from a single entrypoint.
```diff
- import { SlButton } from '@shoelace-style/shoelace/%NPMDIR%/react';
+ import SlButton from '@shoelace-style/shoelace/%NPMDIR%/react/button';
```
You can find a copy + paste import for each component in the "importing" section of its documentation.
### Event Handling
@@ -56,7 +71,7 @@ Here's how you can bind the input's value to a state variable.
```jsx
import { useState } from 'react';
import { SlInput } from '@shoelace-style/shoelace/%NPMDIR%/react';
import SlInput from '@shoelace-style/shoelace/%NPMDIR%/react/input';
function MyComponent() {
const [value, setValue] = useState('');
@@ -71,7 +86,7 @@ If you're using TypeScript, it's important to note that `event.target` will be a
```tsx
import { useState } from 'react';
import { SlInput } from '@shoelace-style/shoelace/%NPMDIR%/react';
import SlInput from '@shoelace-style/shoelace/%NPMDIR%/react/input';
import type SlInputElement from '@shoelace-style/shoelace/%NPMDIR%/components/input/input';
function MyComponent() {
@@ -83,6 +98,25 @@ function MyComponent() {
export default MyComponent;
```
You can also import the event type for use in your callbacks, shown below.
```tsx
import { useCallback, useState } from 'react';
import SlInput, { type SlInputEvent } from '@shoelace-style/shoelace/%NPMDIR%/react/input';
import type SlInputElement from '@shoelace-style/shoelace/%NPMDIR%/components/input/input';
function MyComponent() {
const [value, setValue] = useState('');
const onInput = useCallback((event: SlInputEvent) => {
setValue(event.detail);
}, []);
return <SlInput value={value} onSlInput={event => setValue((event.target as SlInputElement).value)} />;
}
export default MyComponent;
```
## Testing with Jest
Testing with web components can be challenging if your test environment runs in a Node environment (i.e. it doesn't run in a real browser). Fortunately, [Jest](https://jestjs.io/) has made a number of strides to support web components and provide additional browser APIs. However, it's still not a complete replication of a browser environment.
@@ -140,7 +174,7 @@ To fix this, add the following to your `package.json` which tells the transpiler
```js
{
"jest": {
"transformIgnorePatterns": ["node_modules/?!(@shoelace)"]
"transformIgnorePatterns": ["node_modules/(?!(@shoelace))"]
}
}
```

View File

@@ -6,13 +6,13 @@ meta:
# Customizing
Shoelace components can be customized at a high level through design tokens. This gives you control over theme colors and general styling. For more advanced customizations, you can make use of component parts and custom properties to target individual components.
Shoelace components can be customized at a high level through design tokens. This gives you control over theme colors and general styling. For more advanced customizations, you can make use of CSS parts and custom properties to target individual components.
## Design Tokens
Shoelace makes use of several design tokens to provide a consistent appearance across components. You can customize them and use them in your own application with pure CSS — no preprocessor required.
Design tokens offer a high-level way to customize the library with minimal effort. There are no component-specific variables, however, as design tokens are intended to be generic and highly reusable. To customize an individual component, refer to the section entitled [Component Parts](#component-parts).
Design tokens offer a high-level way to customize the library with minimal effort. There are no component-specific variables, however, as design tokens are intended to be generic and highly reusable. To customize an individual component, refer to the section entitled [CSS Parts](#css-parts).
Design tokens are accessed through CSS custom properties that are defined in your theme. Because design tokens live at the page level, they're prefixed with `--sl-` to avoid collisions with other libraries.
@@ -37,9 +37,9 @@ To customize a design token, simply override it in your stylesheet using a `:roo
Many design tokens are described further along in this documentation. For a complete list, refer to `src/themes/light.css` in the project's [source code](https://github.com/shoelace-style/shoelace/blob/current/src/themes/light.css).
## Component Parts
## CSS Parts
Whereas design tokens offer a high-level way to customize the library, component parts offer a low-level way to customize individual components. Again, this is done with pure CSS — no preprocessor required.
Whereas design tokens offer a high-level way to customize the library, CSS parts offer a low-level way to customize individual components. Again, this is done with pure CSS — no preprocessor required.
Shoelace components use a [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM) to encapsulate their styles and behaviors. As a result, you can't simply target their internals with the usual CSS selectors. Instead, components expose "parts" that can be targeted with the [CSS part selector](https://developer.mozilla.org/en-US/docs/Web/CSS/::part), or `::part()`.
@@ -76,7 +76,7 @@ At first glance, this approach might seem a bit verbose or even limiting, but it
- Customizations can be made to components with explicit selectors, such as `::part(icon)`, rather than implicit selectors, such as `.button > div > span + .icon`, that are much more fragile.
- The internal structure of a component will likely change as it evolves. By exposing component parts through an API, the internals can be reworked without fear of breaking customizations as long as its parts remain intact.
- The internal structure of a component will likely change as it evolves. By exposing CSS parts through an API, the internals can be reworked without fear of breaking customizations as long as its parts remain intact.
- It encourages us to think more about how components are designed and how customizations should be allowed before users can take advantage of them. Once we opt a part into the component's API, it's guaranteed to be supported and can't be removed until a major version of the library is released.

View File

@@ -80,15 +80,31 @@ The form will not be submitted if a required field is incomplete.
<script type="module">
const form = document.querySelector('.input-validation-required');
form.addEventListener('submit', event => {
event.preventDefault();
alert('All fields are valid!');
// Wait for controls to be defined before attaching form listeners
await Promise.all([
customElements.whenDefined('sl-button'),
customElements.whenDefined('sl-checkbox'),
customElements.whenDefined('sl-input'),
customElements.whenDefined('sl-option'),
customElements.whenDefined('sl-select'),
customElements.whenDefined('sl-textarea')
]).then(() => {
form.addEventListener('submit', event => {
event.preventDefault();
alert('All fields are valid!');
});
});
</script>
```
```jsx:react
import { SlButton, SlCheckbox, SlInput, SlMenuItem, SlSelect, SlTextarea } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlCheckbox from '@shoelace-style/shoelace/dist/react/checkbox';
import SlInput from '@shoelace-style/shoelace/dist/react/input';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
import SlTextarea from '@shoelace-style/shoelace/dist/react/textarea';
const App = () => {
function handleSubmit(event) {
@@ -134,15 +150,23 @@ To restrict a value to a specific [pattern](https://developer.mozilla.org/en-US/
<script type="module">
const form = document.querySelector('.input-validation-pattern');
form.addEventListener('submit', event => {
event.preventDefault();
alert('All fields are valid!');
// Wait for controls to be defined before attaching form listeners
await Promise.all([
customElements.whenDefined('sl-button'),
customElements.whenDefined('sl-input')
]).then(() => {
form.addEventListener('submit', event => {
event.preventDefault();
alert('All fields are valid!');
});
});
</script>
```
```jsx:react
import { SlButton, SlInput } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlInput from '@shoelace-style/shoelace/dist/react/input';
const App = () => {
function handleSubmit(event) {
@@ -178,15 +202,23 @@ Some input types will automatically trigger constraints, such as `email` and `ur
<script type="module">
const form = document.querySelector('.input-validation-type');
form.addEventListener('submit', event => {
event.preventDefault();
alert('All fields are valid!');
// Wait for controls to be defined before attaching form listeners
await Promise.all([
customElements.whenDefined('sl-button'),
customElements.whenDefined('sl-input')
]).then(() => {
form.addEventListener('submit', event => {
event.preventDefault();
alert('All fields are valid!');
});
});
</script>
```
```jsx:react
import { SlButton, SlInput } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlInput from '@shoelace-style/shoelace/dist/react/input';
const App = () => {
function handleSubmit(event) {
@@ -224,24 +256,31 @@ To create a custom validation error, pass a non-empty string to the `setCustomVa
const form = document.querySelector('.input-validation-custom');
const input = form.querySelector('sl-input');
form.addEventListener('submit', event => {
event.preventDefault();
alert('All fields are valid!');
});
// Wait for controls to be defined before attaching form listeners
await Promise.all([
customElements.whenDefined('sl-button'),
customElements.whenDefined('sl-input')
]).then(() => {
form.addEventListener('submit', event => {
event.preventDefault();
alert('All fields are valid!');
});
input.addEventListener('sl-input', () => {
if (input.value === 'shoelace') {
input.setCustomValidity('');
} else {
input.setCustomValidity("Hey, you're supposed to type 'shoelace' before submitting this!");
}
input.addEventListener('sl-input', () => {
if (input.value === 'shoelace') {
input.setCustomValidity('');
} else {
input.setCustomValidity("Hey, you're supposed to type 'shoelace' before submitting this!");
}
});
});
</script>
```
```jsx:react
import { useRef, useState } from 'react';
import { SlButton, SlInput } from '@shoelace-style/shoelace/dist/react';
import SlButton from '@shoelace-style/shoelace/dist/react/button';
import SlInput from '@shoelace-style/shoelace/dist/react/input';
const App = () => {
const input = useRef(null);
@@ -326,9 +365,19 @@ This example demonstrates custom validation styles using `data-user-invalid` and
<script type="module">
const form = document.querySelector('.validity-styles');
form.addEventListener('submit', event => {
event.preventDefault();
alert('All fields are valid!');
// Wait for controls to be defined before attaching form listeners
await Promise.all([
customElements.whenDefined('sl-button'),
customElements.whenDefined('sl-checkbox'),
customElements.whenDefined('sl-input'),
customElements.whenDefined('sl-option'),
customElements.whenDefined('sl-select')
]).then(() => {
form.addEventListener('submit', event => {
event.preventDefault();
alert('All fields are valid!');
});
});
</script>
@@ -413,37 +462,43 @@ To disable the browser's error messages, you need to cancel the `sl-invalid` eve
<sl-button type="reset" variant="default">Reset</sl-button>
</form>
<script>
<script type="module">
const form = document.querySelector('.inline-validation');
const nameError = document.querySelector('#name-error');
// A form control is invalid
form.addEventListener(
'sl-invalid',
event => {
// Suppress the browser's constraint validation message
// Wait for controls to be defined before attaching form listeners
await Promise.all([
customElements.whenDefined('sl-button'),
customElements.whenDefined('sl-input')
]).then(() => {
// A form control is invalid
form.addEventListener(
'sl-invalid',
event => {
// Suppress the browser's constraint validation message
event.preventDefault();
nameError.textContent = `Error: ${event.target.validationMessage}`;
nameError.hidden = false;
event.target.focus();
},
{ capture: true } // you must use capture since sl-invalid doesn't bubble!
);
// Handle form submit
form.addEventListener('submit', event => {
event.preventDefault();
nameError.hidden = true;
nameError.textContent = '';
setTimeout(() => alert('All fields are valid'), 50);
});
nameError.textContent = `Error: ${event.target.validationMessage}`;
nameError.hidden = false;
event.target.focus();
},
{ capture: true } // you must use capture since sl-invalid doesn't bubble!
);
// Handle form submit
form.addEventListener('submit', event => {
event.preventDefault();
nameError.hidden = true;
nameError.textContent = '';
setTimeout(() => alert('All fields are valid'), 50);
});
// Handle form reset
form.addEventListener('reset', event => {
nameError.hidden = true;
nameError.textContent = '';
// Handle form reset
form.addEventListener('reset', event => {
nameError.hidden = true;
nameError.textContent = '';
});
});
</script>

View File

@@ -112,15 +112,29 @@ However, if you're [cherry picking](#cherry-picking) or [bundling](#bundling) Sh
```
:::tip
When setting a basePath, and easy way to check if it was down properly is by checking if an icon exists.
For example, if I set the basePath to `/dist`, I should be able to go to:
`https://<my-site>/dist/assets/icons/arrow-left.svg` and the browser should show me the SVG.
Shoelace also exports a `getBasePath()` method you can use to reference assets.
An easy way to make sure the base path is configured properly is to check if [icons](/components/icon) are loading.
:::
### Referencing Assets
Most of the magic behind assets is handled internally by Shoelace, but if you need to reference the base path for any reason, the same module exports a function called `getBasePath()`. An optional string argument can be passed, allowing you to get the full path to any asset.
```html
<script type="module">
import { getBasePath, setBasePath } from '@shoelace-style/shoelace/%NPMDIR%/utilities/base-path.js';
setBasePath('/path/to/assets');
// ...
// Get the base path, e.g. /path/to/assets
const basePath = getBasePath();
// Get the path to an asset, e.g. /path/to/assets/file.ext
const assetPath = getBasePath('file.ext');
</script>
```
## Cherry Picking
Cherry picking can be done from [the CDN](#cdn-installation-easiest) or from [npm](#npm-installation). This approach will load only the components you need up front, while limiting the number of files the browser has to download. The disadvantage is that you need to import each individual component.
@@ -173,7 +187,7 @@ import '@shoelace-style/shoelace/%NPMDIR%/components/rating/rating.js';
import { setBasePath } from '@shoelace-style/shoelace/%NPMDIR%/utilities/base-path.js';
// Set the base path to the folder you copied Shoelace's assets to
setBasePath('/path/to/shoelace/%NPMDIR%
setBasePath('/path/to/shoelace/%NPMDIR%');
// <sl-button>, <sl-icon>, <sl-input>, and <sl-rating> are ready to use!
```
@@ -182,6 +196,21 @@ setBasePath('/path/to/shoelace/%NPMDIR%
Component modules include side effects for registration purposes. Because of this, importing directly from `@shoelace-style/shoelace` may result in a larger bundle size than necessary. For optimal tree shaking, always cherry pick, i.e. import components and utilities from their respective files, as shown above.
:::
### Avoiding auto-registering imports
By default, imports to components will auto-register themselves. This may not be ideal in all cases. To import just the component's class without auto-registering it's tag we can do the following:
```diff
- import SlButton from '@shoelace-style/shoelace/%NPMDIR%/components/button/button.js';
+ import SlButton from '@shoelace-style/shoelace/%NPMDIR%/components/button/button.component.js';
```
Notice how the import ends with `.component.js`. This is the current convention to convey the import does not register itself.
:::danger
While you can override the class or re-register the shoelace class under a different tag name, if you do so, many components wont work as expected.
:::
## The difference between CDN and npm
You'll notice that the CDN links all start with `/%CDNDIR%/<path>` and npm imports use `/%NPMDIR%/<path>`. The `/%CDNDIR%` files are bundled separately from the `/%NPMDIR%` files. The `/%CDNDIR%` files come pre-bundled, which means all dependencies are inlined so you do not need to worry about loading additional libraries. The `/%NPMDIR%` files **DO NOT** come pre-bundled, allowing your bundler of choice to more efficiently deduplicate dependencies, resulting in smaller bundles and optimal code sharing.

View File

@@ -210,6 +210,12 @@ Shoelace ships with a file called `vscode.html-custom-data.json` that can be use
If `settings.json` already exists, simply add the above line to the root of the object. Note that you may need to restart VS Code for the changes to take affect.
## JetBrains IDEs
If you are using a [JetBrains IDE](https://www.jetbrains.com/) and you are installing Shoelace from NPM, the editor will automatically detect the `web-types.json` file from the package and you should immediately see component information in your editor.
If you are installing from the CDN, you can [download a local copy](https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace/cdn/web-types.json) and add it to the root of your project.
### Other Editors
Most popular editors support custom code completion with a bit of configuration. Please [submit a feature request](https://github.com/shoelace-style/shoelace/issues/new/choose) for your editor of choice. PRs are also welcome!

View File

@@ -27,6 +27,8 @@ toc: false
</div>
</div>
<div class="badges">
[![jsDelivr](https://data.jsdelivr.com/v1/package/npm/@shoelace-style/shoelace/badge)](https://www.jsdelivr.com/package/npm/@shoelace-style/shoelace)
[![npm](https://img.shields.io/npm/dw/@shoelace-style/shoelace?label=npm&style=flat-square)](https://www.npmjs.com/package/@shoelace-style/shoelace)
[![License](https://img.shields.io/badge/license-MIT-232323.svg?style=flat-square)](https://github.com/shoelace-style/shoelace/blob/next/LICENSE.md)<br>
@@ -34,6 +36,8 @@ toc: false
[![Twitter](https://img.shields.io/badge/Twitter-Follow-00acee.svg?style=flat-square&logo=twitter&logoColor=white)](https://twitter.com/shoelace_style)
[![Sponsor](https://img.shields.io/badge/GitHub-Code-232323.svg?style=flat-square&logo=github&logoColor=white)](https://github.com/shoelace-style/shoelace)
</div>
## Quick Start
Add the following code to your page.

View File

@@ -14,15 +14,63 @@ New versions of Shoelace are released as-needed and generally occur when a criti
## Next
- Fixed a bug in `<sl-switch>` that resulted in improper spacing between the label and the required asterisk [#1540]
- Updated `@ctrl/tinycolor` to 4.0.1 [#1542]
## 2.8.0
- Added `--isolatedModules` and `--verbatimModuleSyntax` to `tsconfig.json`. For anyone directly importing event types, they no longer provide a default export due to these options being enabled. For people using the `events/event.js` file directly, there is no change.
- Added support for submenus in `<sl-menu-item>` [#1410]
- Added the `--submenu-offset` custom property to `<sl-menu-item>` [#1410]
- Fixed an issue with focus trapping elements like `<sl-dialog>` when wrapped by other elements not checking the assigned elements of `<slot>`s. [#1537]
- Fixed type issues with the `ref` attribute in React Wrappers. [#1526]
- Fixed a regression that caused `<sl-radio-button>` to render incorrectly with gaps [#1523]
- Improved expand/collapse behavior of `<sl-tree>` to work more like users expect [#1521]
- Improved `<sl-menu-item>` so labels truncate properly instead of getting chopped and overflowing
- Removed the extra `React.Component` around `@lit-labs/react` wrapper. [#1531]
- Updated `@lit-labs/react` to v2.0.1. [#1531]
## 2.7.0
- Added the experimental `<sl-copy-button>` component [#1473]
- Fixed a bug in `<sl-dropdown>` where pressing [[Up]] or [[Down]] when focused on the trigger wouldn't focus the first/last menu items [#1472]
- Fixed a bug that caused key presses in text fields to be hijacked when used inside `<sl-tree>` [#1492]
- Fixed an upstream bug that caused React CodePen examples to stop working
- Improved the behavior of the clear button in `<sl-input>` to prevent the component's width from shifting when toggled [#1496]
- Improved `<sl-tooltip>` to prevent user selection so the tooltip doesn't get highlighted when dragging selections
- Moved tag type definitions out of component files and into definition files
- Removed `sideEffects` key from `package.json`. Update React docs to use cherry-picking. [#1485]
- Updated Bootstrap Icons to 1.10.5
## 2.6.0
- Added JSDoc comments to React Wrappers for better documentation when hovering a component. [#1450]
- Added `displayName` to React Wrappers for better debugging. [#1450]
- Added non-auto-registering routes for Components to fix a number of issues around auto-registration. [#1450]
- Added a console warning if you attempt to register the same Shoelace component twice. [#1450]
- Added tests for `<sl-qr-code>` [#1416]
- Added support for pressing [[Space]] to select/toggle selected `<sl-menu-item>` elements [#1429]
- Added support for virtual elements in `<sl-popup>` [#1449]
- Added the `spinner` part to `<sl-button>` [#1460]
- Added a `shoelace.js` and `shoelace-autoloader.js` to exportmaps. [#1450]
- Added types to events emitted by React wrapped components [#1419]
- Fixed React component treeshaking by introducing `sideEffects` key in `package.json`. [#1450]
- Fixed a bug in `<sl-tree>` where it was auto-defining `<sl-tree-item>`. [#1450]
- Fixed a bug in focus trapping of modal elements like `<sl-dialog>`. We now manually handle focus ordering as well as added `offsetParent()` check for tabbable boundaries in Safari. Test cases added for `<sl-dialog>` inside a shadowRoot [#1403]
- Fixed a bug in `valueAsDate` on `<sl-input>` where it would always set `type="date"` for the underlying `<input>` element. It now falls back to the native browser implementation for the in-memory input. This may cause unexpected behavior if you're using `valueAsDate` on any input elements that aren't `type="date"`. [#1399]
- Fixed a bug in `<sl-qr-code>` where the `background` attribute was never passed to the QR code [#1416]
- Fixed a bug in `<sl-dropdown>` where aria attributes were incorrectly applied to the default `<slot>` causing Lighthouse errors [#1417]
- Fixed a bug in `<sl-carousel>` that caused navigation to work incorrectly in some case [#1420]
- Fixed a number of slots that incorrectly had aria- and/or role attributes directly on them [#1422]
- Fixed a bug in `<sl-tree>` that caused focus to be stolen when removing focused tree items [#1430]
- Fixed a bug in `<sl-dialog>` and `<sl-drawer>` that caused nested modals to respond too eagerly to the [[Esc]] key [#1457]
- Improved `<sl-details>` to use `<details>` internally for better semantics and to enable search to find in supportive browsers when collapsed [#1470]
- Updated ESLint and related plugins to the latest versions
- Changed the default entrypoint for jsDelivr to point to the autoloader. [#1450]
## 2.5.2
- Fixed broken source buttons in the docs [#1401]
- Fixed broken links in the docs [#1407]
## 2.5.1

View File

@@ -367,7 +367,6 @@ Then use the following syntax for comments so they appear in the generated docs.
* @cssproperty --color: The component's text color.
* @cssproperty --background-color: The component's background color.
*/
@customElement('sl-example')
export default class SlExample {
// ...
}

View File

@@ -18,146 +18,146 @@ Currently, the source of design tokens is considered to be [`light.css`](https:/
Focus ring tokens control the appearance of focus rings. Note that form inputs use `--sl-input-focus-ring-*` tokens instead.
| Token | Value |
| ------------------------ | ------------------------------------------------------------------------------------- |
| `--sl-focus-ring-color` | var(--sl-color-primary-600) (light theme)<br>var(--sl-color-primary-700) (dark theme) |
| `--sl-focus-ring-style` | solid |
| `--sl-focus-ring-width` | 3px |
| `--sl-focus-ring` | var(--sl-focus-ring-style) var(--sl-focus-ring-width) var(--sl-focus-ring-color) |
| `--sl-focus-ring-offset` | 1px |
| Token | Value |
| ------------------------ | ----------------------------------------------------------------------------------------- |
| `--sl-focus-ring-color` | `var(--sl-color-primary-600)` (light theme)<br>`var(--sl-color-primary-700)` (dark theme) |
| `--sl-focus-ring-style` | `solid` |
| `--sl-focus-ring-width` | `3px` |
| `--sl-focus-ring` | `var(--sl-focus-ring-style) var(--sl-focus-ring-width) var(--sl-focus-ring-color)` |
| `--sl-focus-ring-offset` | `1px` |
## Buttons
Button tokens control the appearance of buttons. In addition, buttons also currently use some form input tokens such as `--sl-input-height-*` and `--sl-input-border-*`. More button tokens may be added in the future to make it easier to style them more independently.
| Token | Value |
| ------------------------------ | --------------------------- |
| `--sl-button-font-size-small` | var(--sl-font-size-x-small) |
| `--sl-button-font-size-medium` | var(--sl-font-size-small) |
| `--sl-button-font-size-large` | var(--sl-font-size-medium) |
| Token | Value |
| ------------------------------ | ----------------------------- |
| `--sl-button-font-size-small` | `var(--sl-font-size-x-small)` |
| `--sl-button-font-size-medium` | `var(--sl-font-size-small)` |
| `--sl-button-font-size-large` | `var(--sl-font-size-medium)` |
## Form Inputs
Form input tokens control the appearance of form controls such as [input](/components/input), [select](/components/select), [textarea](/components/textarea), etc.
| Token | Value |
| --------------------------------------- | -------------------------------- |
| `--sl-input-height-small` | 1.875rem; (30px @ 16px base) |
| `--sl-input-height-medium` | 2.5rem; (40px @ 16px base) |
| `--sl-input-height-large` | 3.125rem; (50px @ 16px base) |
| `--sl-input-background-color` | var(--sl-color-neutral-0) |
| `--sl-input-background-color-hover` | var(--sl-input-background-color) |
| `--sl-input-background-color-focus` | var(--sl-input-background-color) |
| `--sl-input-background-color-disabled` | var(--sl-color-neutral-100) |
| `--sl-input-border-color` | var(--sl-color-neutral-300) |
| `--sl-input-border-color-hover` | var(--sl-color-neutral-400) |
| `--sl-input-border-color-focus` | var(--sl-color-primary-500) |
| `--sl-input-border-color-disabled` | var(--sl-color-neutral-300) |
| `--sl-input-border-width` | 1px |
| `--sl-input-required-content` | "\*" |
| `--sl-input-required-content-offset` | -2px |
| `--sl-input-required-content-color` | var(--sl-input-label-color) |
| `--sl-input-border-radius-small` | var(--sl-border-radius-medium) |
| `--sl-input-border-radius-medium` | var(--sl-border-radius-medium) |
| `--sl-input-border-radius-large` | var(--sl-border-radius-medium) |
| `--sl-input-font-family` | var(--sl-font-sans) |
| `--sl-input-font-weight` | var(--sl-font-weight-normal) |
| `--sl-input-font-size-small` | var(--sl-font-size-small) |
| `--sl-input-font-size-medium` | var(--sl-font-size-medium) |
| `--sl-input-font-size-large` | var(--sl-font-size-large) |
| `--sl-input-letter-spacing` | var(--sl-letter-spacing-normal) |
| `--sl-input-color` | var(--sl-color-neutral-700) |
| `--sl-input-color-hover` | var(--sl-color-neutral-700) |
| `--sl-input-color-focus` | var(--sl-color-neutral-700) |
| `--sl-input-color-disabled` | var(--sl-color-neutral-900) |
| `--sl-input-icon-color` | var(--sl-color-neutral-500) |
| `--sl-input-icon-color-hover` | var(--sl-color-neutral-600) |
| `--sl-input-icon-color-focus` | var(--sl-color-neutral-600) |
| `--sl-input-placeholder-color` | var(--sl-color-neutral-500) |
| `--sl-input-placeholder-color-disabled` | var(--sl-color-neutral-600) |
| `--sl-input-spacing-small` | var(--sl-spacing-small) |
| `--sl-input-spacing-medium` | var(--sl-spacing-medium) |
| `--sl-input-spacing-large` | var(--sl-spacing-large) |
| `--sl-input-focus-ring-color` | hsl(198.6 88.7% 48.4% / 40%) |
| `--sl-input-focus-ring-offset` | 0 |
| Token | Value |
| --------------------------------------- | ---------------------------------- |
| `--sl-input-height-small` | `1.875rem` (30px @ 16px base) |
| `--sl-input-height-medium` | `2.5rem` (40px @ 16px base) |
| `--sl-input-height-large` | `3.125rem` (50px @ 16px base) |
| `--sl-input-background-color` | `var(--sl-color-neutral-0)` |
| `--sl-input-background-color-hover` | `var(--sl-input-background-color)` |
| `--sl-input-background-color-focus` | `var(--sl-input-background-color)` |
| `--sl-input-background-color-disabled` | `var(--sl-color-neutral-100)` |
| `--sl-input-border-color` | `var(--sl-color-neutral-300)` |
| `--sl-input-border-color-hover` | `var(--sl-color-neutral-400)` |
| `--sl-input-border-color-focus` | `var(--sl-color-primary-500)` |
| `--sl-input-border-color-disabled` | `var(--sl-color-neutral-300)` |
| `--sl-input-border-width` | `1px` |
| `--sl-input-required-content` | `*` |
| `--sl-input-required-content-offset` | `-2px` |
| `--sl-input-required-content-color` | `var(--sl-input-label-color)` |
| `--sl-input-border-radius-small` | `var(--sl-border-radius-medium)` |
| `--sl-input-border-radius-medium` | `var(--sl-border-radius-medium)` |
| `--sl-input-border-radius-large` | `var(--sl-border-radius-medium)` |
| `--sl-input-font-family` | `var(--sl-font-sans)` |
| `--sl-input-font-weight` | `var(--sl-font-weight-normal)` |
| `--sl-input-font-size-small` | `var(--sl-font-size-small)` |
| `--sl-input-font-size-medium` | `var(--sl-font-size-medium)` |
| `--sl-input-font-size-large` | `var(--sl-font-size-large)` |
| `--sl-input-letter-spacing` | `var(--sl-letter-spacing-normal)` |
| `--sl-input-color` | `var(--sl-color-neutral-700)` |
| `--sl-input-color-hover` | `var(--sl-color-neutral-700)` |
| `--sl-input-color-focus` | `var(--sl-color-neutral-700)` |
| `--sl-input-color-disabled` | `var(--sl-color-neutral-900)` |
| `--sl-input-icon-color` | `var(--sl-color-neutral-500)` |
| `--sl-input-icon-color-hover` | `var(--sl-color-neutral-600)` |
| `--sl-input-icon-color-focus` | `var(--sl-color-neutral-600)` |
| `--sl-input-placeholder-color` | `var(--sl-color-neutral-500)` |
| `--sl-input-placeholder-color-disabled` | `var(--sl-color-neutral-600)` |
| `--sl-input-spacing-small` | `var(--sl-spacing-small)` |
| `--sl-input-spacing-medium` | `var(--sl-spacing-medium)` |
| `--sl-input-spacing-large` | `var(--sl-spacing-large)` |
| `--sl-input-focus-ring-color` | `hsl(198.6 88.7% 48.4% / 40%)` |
| `--sl-input-focus-ring-offset` | `0` |
## Filled Form Inputs
Filled form input tokens control the appearance of form controls using the `filled` variant.
| Token | Value |
| --------------------------------------------- | --------------------------- |
| `--sl-input-filled-background-color` | var(--sl-color-neutral-100) |
| `--sl-input-filled-background-color-hover` | var(--sl-color-neutral-100) |
| `--sl-input-filled-background-color-focus` | var(--sl-color-neutral-100) |
| `--sl-input-filled-background-color-disabled` | var(--sl-color-neutral-100) |
| `--sl-input-filled-color` | var(--sl-color-neutral-800) |
| `--sl-input-filled-color-hover` | var(--sl-color-neutral-800) |
| `--sl-input-filled-color-focus` | var(--sl-color-neutral-700) |
| `--sl-input-filled-color-disabled` | var(--sl-color-neutral-800) |
| Token | Value |
| --------------------------------------------- | ----------------------------- |
| `--sl-input-filled-background-color` | `var(--sl-color-neutral-100)` |
| `--sl-input-filled-background-color-hover` | `var(--sl-color-neutral-100)` |
| `--sl-input-filled-background-color-focus` | `var(--sl-color-neutral-100)` |
| `--sl-input-filled-background-color-disabled` | `var(--sl-color-neutral-100)` |
| `--sl-input-filled-color` | `var(--sl-color-neutral-800)` |
| `--sl-input-filled-color-hover` | `var(--sl-color-neutral-800)` |
| `--sl-input-filled-color-focus` | `var(--sl-color-neutral-700)` |
| `--sl-input-filled-color-disabled` | `var(--sl-color-neutral-800)` |
## Form Labels
Form label tokens control the appearance of labels in form controls.
| Token | Value |
| ----------------------------------- | -------------------------- |
| `--sl-input-label-font-size-small` | var(--sl-font-size-small) |
| `--sl-input-label-font-size-medium` | var(--sl-font-size-medium) |
| `--sl-input-label-font-size-large` | var(--sl-font-size-large) |
| `--sl-input-label-color` | inherit |
| Token | Value |
| ----------------------------------- | ---------------------------- |
| `--sl-input-label-font-size-small` | `var(--sl-font-size-small)` |
| `--sl-input-label-font-size-medium` | `var(--sl-font-size-medium`) |
| `--sl-input-label-font-size-large` | `var(--sl-font-size-large)` |
| `--sl-input-label-color` | `inherit` |
## Help Text
Help text tokens control the appearance of help text in form controls.
| Token | Value |
| --------------------------------------- | --------------------------- |
| `--sl-input-help-text-font-size-small` | var(--sl-font-size-x-small) |
| `--sl-input-help-text-font-size-medium` | var(--sl-font-size-small) |
| `--sl-input-help-text-font-size-large` | var(--sl-font-size-medium) |
| `--sl-input-help-text-color` | var(--sl-color-neutral-500) |
| Token | Value |
| --------------------------------------- | ----------------------------- |
| `--sl-input-help-text-font-size-small` | `var(--sl-font-size-x-small)` |
| `--sl-input-help-text-font-size-medium` | `var(--sl-font-size-small)` |
| `--sl-input-help-text-font-size-large` | `var(--sl-font-size-medium)` |
| `--sl-input-help-text-color` | `var(--sl-color-neutral-500)` |
## Toggles
Toggle tokens control the appearance of toggles such as [checkbox](/components/checkbox), [radio](/components/radio), [switch](/components/switch), etc.
| Token | Value |
| ------------------------- | --------------------------- |
| `--sl-toggle-size-small` | 0.875rem (14px @ 16px base) |
| `--sl-toggle-size-medium` | 1.125rem (18px @ 16px base) |
| `--sl-toggle-size-large` | 1.375rem (22px @ 16px base) |
| Token | Value |
| ------------------------- | ----------------------------- |
| `--sl-toggle-size-small` | `0.875rem` (14px @ 16px base) |
| `--sl-toggle-size-medium` | `1.125rem` (18px @ 16px base) |
| `--sl-toggle-size-large` | `1.375rem` (22px @ 16px base) |
## Overlays
Overlay tokens control the appearance of overlays as used in [dialog](/components/dialog), [drawer](/components/drawer), etc.
| Token | Value |
| ------------------------------- | ------------------------- |
| `--sl-overlay-background-color` | hsl(240 3.8% 46.1% / 33%) |
| Token | Value |
| ------------------------------- | --------------------------- |
| `--sl-overlay-background-color` | `hsl(240 3.8% 46.1% / 33%)` |
## Panels
Panel tokens control the appearance of panels such as those used in [dialog](/components/dialog), [drawer](/components/drawer), [menu](/components/menu), etc.
| Token | Value |
| ----------------------------- | --------------------------- |
| `--sl-panel-background-color` | var(--sl-color-neutral-0) |
| `--sl-panel-border-color` | var(--sl-color-neutral-200) |
| `--sl-panel-border-width` | 1px |
| Token | Value |
| ----------------------------- | ----------------------------- |
| `--sl-panel-background-color` | `var(--sl-color-neutral-0)` |
| `--sl-panel-border-color` | `var(--sl-color-neutral-200)` |
| `--sl-panel-border-width` | `1px` |
## Tooltips
Tooltip tokens control the appearance of tooltips. This includes the [tooltip](/components/tooltip) component as well as other implementations, such [range tooltips](/components/range).
| Token | Value |
| ------------------------------- | ---------------------------------------------------- |
| `--sl-tooltip-border-radius` | var(--sl-border-radius-medium) |
| `--sl-tooltip-background-color` | var(--sl-color-neutral-800) |
| `--sl-tooltip-color` | var(--sl-color-neutral-0) |
| `--sl-tooltip-font-family` | var(--sl-font-sans) |
| `--sl-tooltip-font-weight` | var(--sl-font-weight-normal) |
| `--sl-tooltip-font-size` | var(--sl-font-size-small) |
| `--sl-tooltip-line-height` | var(--sl-line-height-dense) |
| `--sl-tooltip-padding` | var(--sl-spacing-2x-small) var(--sl-spacing-x-small) |
| `--sl-tooltip-arrow-size` | 6px |
| Token | Value |
| ------------------------------- | ------------------------------------------------------ |
| `--sl-tooltip-border-radius` | `var(--sl-border-radius-medium)` |
| `--sl-tooltip-background-color` | `var(--sl-color-neutral-800)` |
| `--sl-tooltip-color` | `var(--sl-color-neutral-0)` |
| `--sl-tooltip-font-family` | `var(--sl-font-sans)` |
| `--sl-tooltip-font-weight` | `var(--sl-font-weight-normal)` |
| `--sl-tooltip-font-size` | `var(--sl-font-size-small)` |
| `--sl-tooltip-line-height` | `var(--sl-line-height-dense)` |
| `--sl-tooltip-padding` | `var(--sl-spacing-2x-small) var(--sl-spacing-x-small)` |
| `--sl-tooltip-arrow-size` | `6px` |

1029
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +1,23 @@
{
"name": "@shoelace-style/shoelace",
"description": "A forward-thinking library of web components.",
"version": "2.5.2",
"version": "2.8.0",
"homepage": "https://github.com/shoelace-style/shoelace",
"author": "Cory LaViska",
"license": "MIT",
"customElements": "dist/custom-elements.json",
"web-types": "dist/web-types.json",
"web-types": "./web-types.json",
"type": "module",
"types": "dist/shoelace.d.ts",
"jsdelivr": "./cdn/shoelace-autoloader.js",
"exports": {
".": {
"types": "./dist/shoelace.d.ts",
"import": "./dist/shoelace.js"
},
"./dist/custom-elements.json": "./dist/custom-elements.json",
"./dist/shoelace.js": "./dist/shoelace.js",
"./dist/shoelace-autoloader.js": "./dist/shoelace-autoloader.js",
"./dist/themes/*": "./dist/themes/*",
"./dist/components/*": "./dist/components/*",
"./dist/utilities/*": "./dist/utilities/*",
@@ -22,15 +25,8 @@
"./dist/react/*": "./dist/react/*",
"./dist/translations/*": "./dist/translations/*"
},
"files": [
"dist",
"cdn"
],
"keywords": [
"web components",
"custom elements",
"components"
],
"files": ["dist", "cdn"],
"keywords": ["web components", "custom elements", "components"],
"repository": {
"type": "git",
"url": "git+https://github.com/shoelace-style/shoelace.git"
@@ -64,9 +60,9 @@
"node": ">=14.17.0"
},
"dependencies": {
"@ctrl/tinycolor": "^3.5.0",
"@ctrl/tinycolor": "^4.0.1",
"@floating-ui/dom": "^1.2.1",
"@lit-labs/react": "^1.1.1",
"@lit-labs/react": "^2.0.1",
"@shoelace-style/animations": "^1.1.0",
"@shoelace-style/localize": "^3.1.1",
"composed-offset-position": "^0.0.4",
@@ -79,32 +75,34 @@
"@open-wc/testing": "^3.1.7",
"@types/mocha": "^10.0.1",
"@types/react": "^18.0.26",
"@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.48.1",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@web/dev-server-esbuild": "^0.3.3",
"@web/test-runner": "^0.15.0",
"@web/test-runner-commands": "^0.6.5",
"@web/test-runner-playwright": "^0.9.0",
"bootstrap-icons": "^1.10.3",
"bootstrap-icons": "^1.10.5",
"browser-sync": "^2.29.3",
"cem-plugin-vs-code-custom-data-generator": "^1.4.1",
"chalk": "^5.2.0",
"change-case": "^4.1.2",
"command-line-args": "^5.2.1",
"comment-parser": "^1.3.1",
"cspell": "^6.18.1",
"custom-element-jet-brains-integration": "^1.1.0",
"custom-element-vs-code-integration": "^1.1.0",
"del": "^7.0.0",
"download": "^8.0.0",
"esbuild": "^0.18.2",
"eslint": "^8.31.0",
"esbuild-plugin-replace": "^1.4.0",
"eslint": "^8.44.0",
"eslint-plugin-chai-expect": "^3.0.0",
"eslint-plugin-chai-friendly": "^0.7.2",
"eslint-plugin-import": "^2.27.4",
"eslint-plugin-lit": "^1.8.2",
"eslint-plugin-lit-a11y": "^2.3.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-lit": "^1.8.3",
"eslint-plugin-lit-a11y": "^4.1.0",
"eslint-plugin-markdown": "^3.0.0",
"eslint-plugin-sort-imports-es6-autofix": "^0.6.0",
"eslint-plugin-wc": "^1.4.0",
"eslint-plugin-wc": "^1.5.0",
"front-matter": "^4.0.2",
"get-port": "^7.0.0",
"globby": "^13.1.3",
@@ -135,9 +133,6 @@
"user-agent-data-types": "^0.3.0"
},
"lint-staged": {
"*.{ts,js}": [
"eslint --max-warnings 0 --cache --fix",
"prettier --write"
]
"*.{ts,js}": ["eslint --max-warnings 0 --cache --fix", "prettier --write"]
}
}

View File

@@ -11,6 +11,8 @@ import getPort, { portNumbers } from 'get-port';
import ora from 'ora';
import util from 'util';
import * as path from 'path';
import { readFileSync } from 'fs';
import { replace } from 'esbuild-plugin-replace';
const { serve } = commandLineArgs([{ name: 'serve', type: Boolean }]);
const outdir = 'dist';
@@ -22,6 +24,8 @@ let childProcess;
let buildResults;
const bundleDirectories = [cdndir, outdir];
let packageData = JSON.parse(readFileSync(path.join(process.cwd(), 'package.json'), 'utf-8'));
const shoelaceVersion = JSON.stringify(packageData.version.toString());
//
// Runs 11ty and builds the docs. The returned promise resolves after the initial publish has completed. The child
@@ -49,6 +53,10 @@ async function buildTheDocs(watch = false) {
output.push(data.toString());
});
child.stderr.on('data', data => {
output.push(data.toString());
});
if (watch) {
// The process doesn't terminate in watch mode so, before resolving, we listen for a known signal in stdout that
// tells us when the first build completes.
@@ -108,13 +116,18 @@ async function buildTheSource() {
//
external: alwaysExternal,
splitting: true,
plugins: []
plugins: [
replace({
__SHOELACE_VERSION__: shoelaceVersion
})
]
};
const npmConfig = {
...cdnConfig,
bundle: false,
external: undefined,
minify: false,
packages: 'external',
outdir
};
@@ -179,10 +192,6 @@ await nextTask('Wrapping components for React', () => {
return execPromise(`node scripts/make-react.js --outdir "${outdir}"`, { stdio: 'inherit' });
});
await nextTask('Generating Web Types', () => {
return execPromise(`node scripts/make-web-types.js --outdir "${outdir}"`, { stdio: 'inherit' });
});
await nextTask('Generating themes', () => {
return execPromise(`node scripts/make-themes.js --outdir "${outdir}"`, { stdio: 'inherit' });
});
@@ -198,6 +207,7 @@ await nextTask('Running the TypeScript compiler', () => {
// Copy the above steps to the CDN directory directly so we don't need to twice the work for nothing.
await nextTask(`Copying Web Types, Themes, Icons, and TS Types to "${cdndir}"`, async () => {
await deleteAsync(cdndir);
await copy('./web-types.json', `${outdir}/web-types.json`);
await copy(outdir, cdndir);
});
@@ -263,13 +273,16 @@ if (serve) {
});
// Rebuild and reload when source files change
bs.watch(['src/**/!(*.test).*']).on('change', async filename => {
bs.watch('src/**/!(*.test).*').on('change', async filename => {
console.log('[build] File changed: ', filename);
try {
const isTheme = /^src\/themes/.test(filename);
const isStylesheet = /(\.css|\.styles\.ts)$/.test(filename);
// Rebuild the source
await Promise.all([buildResults.map(result => result.rebuild())]);
const rebuildResults = buildResults.map(result => result.rebuild());
await Promise.all(rebuildResults);
// Rebuild stylesheets when a theme file changes
if (isTheme) {

View File

@@ -32,7 +32,7 @@ await deleteAsync([iconDir]);
await fs.mkdir(iconDir, { recursive: true });
await Promise.all([
copy(`${srcPath}/icons`, iconDir),
copy(`${srcPath}/LICENSE.md`, path.join(iconDir, 'LICENSE.md')),
copy(`${srcPath}/LICENSE`, path.join(iconDir, 'LICENSE')),
copy(`${srcPath}/bootstrap-icons.svg`, './docs/assets/images/sprite.svg', { overwrite: true })
]);

View File

@@ -24,25 +24,48 @@ components.map(component => {
const tagWithoutPrefix = component.tagName.replace(/^sl-/, '');
const componentDir = path.join(reactDir, tagWithoutPrefix);
const componentFile = path.join(componentDir, 'index.ts');
const importPath = component.path;
const events = (component.events || []).map(event => `${event.reactName}: '${event.name}'`).join(',\n');
const importPath = component.path.replace(/\.js$/, '.component.js');
const eventImports = (component.events || [])
.map(event => `import type { ${event.eventName} } from '../../../src/events/events';`)
.join('\n');
const eventExports = (component.events || [])
.map(event => `export type { ${event.eventName} } from '../../../src/events/events';`)
.join('\n');
const eventNameImport =
(component.events || []).length > 0 ? `import { type EventName } from '@lit-labs/react';` : ``;
const events = (component.events || [])
.map(event => `${event.reactName}: '${event.name}' as EventName<${event.eventName}>`)
.join(',\n');
fs.mkdirSync(componentDir, { recursive: true });
const jsDoc = component.jsDoc || '';
const source = prettier.format(
`
import * as React from 'react';
import { createComponent } from '@lit-labs/react';
import Component from '../../${importPath}';
export default createComponent({
tagName: '${component.tagName}',
${eventNameImport}
${eventImports}
${eventExports}
const tagName = '${component.tagName}'
Component.define('${component.tagName}')
${jsDoc}
const reactWrapper = createComponent({
tagName,
elementClass: Component,
react: React,
events: {
${events}
}
});
},
displayName: "${component.name}"
})
export default reactWrapper
`,
Object.assign(prettierConfig, {
parser: 'babel-ts'

View File

@@ -1,68 +0,0 @@
//
// This script generates a web-types.json file from custom-elements.json for use with WebStorm/PHPStorm
//
// Docs: https://github.com/JetBrains/web-types
//
import commandLineArgs from 'command-line-args';
import jsonata from 'jsonata';
import fs from 'fs';
import path from 'path';
const { outdir } = commandLineArgs({ name: 'outdir', type: String });
const metadata = JSON.parse(fs.readFileSync(path.join(outdir, 'custom-elements.json'), 'utf8'));
const jsonataExprString = `{
"$schema": "http://json.schemastore.org/web-types",
"name": package.name,
"version": package.version,
"description-markup": "markdown",
"framework-config": {
"enable-when": {
"node-packages": [
package.name
]
}
},
"contributions": {
"html": {
"elements": [
modules.declarations.{
"name": tagName,
"description": description,
"doc-url": $join(["https://shoelace.style/components/", $substringAfter(tagName, 'sl-')]),
"js": {
"properties": [
members.{
"name": name,
"description": description,
"value": {
"type": type.text
}
}
],
"events": [
events.{
"name": name,
"description": description
}
]
},
"attributes": [
attributes.{
"name": name,
"description": description,
"value": {
"type": type.text
}
}
]
}
]
}
}
}`;
const expression = jsonata(jsonataExprString);
const result = await expression.evaluate(metadata);
fs.writeFileSync(path.join(outdir, 'web-types.json'), JSON.stringify(result, null, 2), 'utf8');

View File

@@ -33,6 +33,11 @@ export default function (plop) {
{
type: 'add',
path: '../../src/components/{{ tagWithoutPrefix tag }}/{{ tagWithoutPrefix tag }}.ts',
templateFile: 'templates/component/define.hbs'
},
{
type: 'add',
path: '../../src/components/{{ tagWithoutPrefix tag }}/{{ tagWithoutPrefix tag }}.component.ts',
templateFile: 'templates/component/component.hbs'
},
{

View File

@@ -1,4 +1,4 @@
import { customElement, property } from 'lit/decorators.js';
import { property } from 'lit/decorators.js';
import { html } from 'lit';
import { LocalizeController } from '../../utilities/localize.js';
import { watch } from '../../internal/watch.js';
@@ -23,7 +23,6 @@ import type { CSSResultGroup } from 'lit';
*
* @cssproperty --example - An example CSS custom property.
*/
@customElement('{{ tag }}')
export default class {{ properCase tag }} extends ShoelaceElement {
static styles: CSSResultGroup = styles;
@@ -41,9 +40,3 @@ export default class {{ properCase tag }} extends ShoelaceElement {
return html` <slot></slot> `;
}
}
declare global {
interface HTMLElementTagNameMap {
'{{ tag }}': {{ properCase tag }};
}
}

View File

@@ -0,0 +1,12 @@
import {{ properCase tag }} from './{{ tagWithoutPrefix tag }}.component.js';
export * from './{{ tagWithoutPrefix tag }}.component.js';
export default {{ properCase tag }};
{{ properCase tag }}.define('{{ tag }}');
declare global {
interface HTMLElementTagNameMap {
'{{ tag }}': {{ properCase tag }};
}
}

View File

@@ -5,7 +5,7 @@ meta:
layout: component
---
```html preview
```html:preview
<{{ tag }}></{{ tag }}>
```

View File

@@ -0,0 +1,241 @@
import { animateTo, stopAnimations } from '../../internal/animate.js';
import { classMap } from 'lit/directives/class-map.js';
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry.js';
import { HasSlotController } from '../../internal/slot.js';
import { html } from 'lit';
import { LocalizeController } from '../../utilities/localize.js';
import { property, query } from 'lit/decorators.js';
import { waitForEvent } from '../../internal/event.js';
import { watch } from '../../internal/watch.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import SlIconButton from '../icon-button/icon-button.component.js';
import styles from './alert.styles.js';
import type { CSSResultGroup } from 'lit';
const toastStack = Object.assign(document.createElement('div'), { className: 'sl-toast-stack' });
/**
* @summary Alerts are used to display important messages inline or as toast notifications.
* @documentation https://shoelace.style/components/alert
* @status stable
* @since 2.0
*
* @dependency sl-icon-button
*
* @slot - The alert's main content.
* @slot icon - An icon to show in the alert. Works best with `<sl-icon>`.
*
* @event sl-show - Emitted when the alert opens.
* @event sl-after-show - Emitted after the alert opens and all animations are complete.
* @event sl-hide - Emitted when the alert closes.
* @event sl-after-hide - Emitted after the alert closes and all animations are complete.
*
* @csspart base - The component's base wrapper.
* @csspart icon - The container that wraps the optional icon.
* @csspart message - The container that wraps the alert's main content.
* @csspart close-button - The close button, an `<sl-icon-button>`.
* @csspart close-button__base - The close button's exported `base` part.
*
* @animation alert.show - The animation to use when showing the alert.
* @animation alert.hide - The animation to use when hiding the alert.
*/
export default class SlAlert extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static dependencies = { 'sl-icon-button': SlIconButton };
private autoHideTimeout: number;
private readonly hasSlotController = new HasSlotController(this, 'icon', 'suffix');
private readonly localize = new LocalizeController(this);
@query('[part~="base"]') base: HTMLElement;
/**
* Indicates whether or not the alert is open. You can toggle this attribute to show and hide the alert, or you can
* use the `show()` and `hide()` methods and this attribute will reflect the alert's open state.
*/
@property({ type: Boolean, reflect: true }) open = false;
/** Enables a close button that allows the user to dismiss the alert. */
@property({ type: Boolean, reflect: true }) closable = false;
/** The alert's theme variant. */
@property({ reflect: true }) variant: 'primary' | 'success' | 'neutral' | 'warning' | 'danger' = 'primary';
/**
* The length of time, in milliseconds, the alert will show before closing itself. If the user interacts with
* the alert before it closes (e.g. moves the mouse over it), the timer will restart. Defaults to `Infinity`, meaning
* the alert will not close on its own.
*/
@property({ type: Number }) duration = Infinity;
firstUpdated() {
this.base.hidden = !this.open;
}
private restartAutoHide() {
clearTimeout(this.autoHideTimeout);
if (this.open && this.duration < Infinity) {
this.autoHideTimeout = window.setTimeout(() => this.hide(), this.duration);
}
}
private handleCloseClick() {
this.hide();
}
private handleMouseMove() {
this.restartAutoHide();
}
@watch('open', { waitUntilFirstUpdate: true })
async handleOpenChange() {
if (this.open) {
// Show
this.emit('sl-show');
if (this.duration < Infinity) {
this.restartAutoHide();
}
await stopAnimations(this.base);
this.base.hidden = false;
const { keyframes, options } = getAnimation(this, 'alert.show', { dir: this.localize.dir() });
await animateTo(this.base, keyframes, options);
this.emit('sl-after-show');
} else {
// Hide
this.emit('sl-hide');
clearTimeout(this.autoHideTimeout);
await stopAnimations(this.base);
const { keyframes, options } = getAnimation(this, 'alert.hide', { dir: this.localize.dir() });
await animateTo(this.base, keyframes, options);
this.base.hidden = true;
this.emit('sl-after-hide');
}
}
@watch('duration')
handleDurationChange() {
this.restartAutoHide();
}
/** Shows the alert. */
async show() {
if (this.open) {
return undefined;
}
this.open = true;
return waitForEvent(this, 'sl-after-show');
}
/** Hides the alert */
async hide() {
if (!this.open) {
return undefined;
}
this.open = false;
return waitForEvent(this, 'sl-after-hide');
}
/**
* Displays the alert as a toast notification. This will move the alert out of its position in the DOM and, when
* dismissed, it will be removed from the DOM completely. By storing a reference to the alert, you can reuse it by
* calling this method again. The returned promise will resolve after the alert is hidden.
*/
async toast() {
return new Promise<void>(resolve => {
if (toastStack.parentElement === null) {
document.body.append(toastStack);
}
toastStack.appendChild(this);
// Wait for the toast stack to render
requestAnimationFrame(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions -- force a reflow for the initial transition
this.clientWidth;
this.show();
});
this.addEventListener(
'sl-after-hide',
() => {
toastStack.removeChild(this);
resolve();
// Remove the toast stack from the DOM when there are no more alerts
if (toastStack.querySelector('sl-alert') === null) {
toastStack.remove();
}
},
{ once: true }
);
});
}
render() {
return html`
<div
part="base"
class=${classMap({
alert: true,
'alert--open': this.open,
'alert--closable': this.closable,
'alert--has-icon': this.hasSlotController.test('icon'),
'alert--primary': this.variant === 'primary',
'alert--success': this.variant === 'success',
'alert--neutral': this.variant === 'neutral',
'alert--warning': this.variant === 'warning',
'alert--danger': this.variant === 'danger'
})}
role="alert"
aria-hidden=${this.open ? 'false' : 'true'}
@mousemove=${this.handleMouseMove}
>
<div part="icon" class="alert__icon">
<slot name="icon"></slot>
</div>
<div part="message" class="alert__message" aria-live="polite">
<slot></slot>
</div>
${this.closable
? html`
<sl-icon-button
part="close-button"
exportparts="base:close-button__base"
class="alert__close-button"
name="x-lg"
library="system"
label=${this.localize.term('close')}
@click=${this.handleCloseClick}
></sl-icon-button>
`
: ''}
</div>
`;
}
}
setDefaultAnimation('alert.show', {
keyframes: [
{ opacity: 0, scale: 0.8 },
{ opacity: 1, scale: 1 }
],
options: { duration: 250, easing: 'ease' }
});
setDefaultAnimation('alert.hide', {
keyframes: [
{ opacity: 1, scale: 1 },
{ opacity: 0, scale: 0.8 }
],
options: { duration: 250, easing: 'ease' }
});

View File

@@ -1,241 +1,9 @@
import '../icon-button/icon-button.js';
import { animateTo, stopAnimations } from '../../internal/animate.js';
import { classMap } from 'lit/directives/class-map.js';
import { customElement, property, query } from 'lit/decorators.js';
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry.js';
import { HasSlotController } from '../../internal/slot.js';
import { html } from 'lit';
import { LocalizeController } from '../../utilities/localize.js';
import { waitForEvent } from '../../internal/event.js';
import { watch } from '../../internal/watch.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import styles from './alert.styles.js';
import type { CSSResultGroup } from 'lit';
import SlAlert from './alert.component.js';
const toastStack = Object.assign(document.createElement('div'), { className: 'sl-toast-stack' });
export * from './alert.component.js';
export default SlAlert;
/**
* @summary Alerts are used to display important messages inline or as toast notifications.
* @documentation https://shoelace.style/components/alert
* @status stable
* @since 2.0
*
* @dependency sl-icon-button
*
* @slot - The alert's main content.
* @slot icon - An icon to show in the alert. Works best with `<sl-icon>`.
*
* @event sl-show - Emitted when the alert opens.
* @event sl-after-show - Emitted after the alert opens and all animations are complete.
* @event sl-hide - Emitted when the alert closes.
* @event sl-after-hide - Emitted after the alert closes and all animations are complete.
*
* @csspart base - The component's base wrapper.
* @csspart icon - The container that wraps the optional icon.
* @csspart message - The container that wraps the alert's main content.
* @csspart close-button - The close button, an `<sl-icon-button>`.
* @csspart close-button__base - The close button's exported `base` part.
*
* @animation alert.show - The animation to use when showing the alert.
* @animation alert.hide - The animation to use when hiding the alert.
*/
@customElement('sl-alert')
export default class SlAlert extends ShoelaceElement {
static styles: CSSResultGroup = styles;
private autoHideTimeout: number;
private readonly hasSlotController = new HasSlotController(this, 'icon', 'suffix');
private readonly localize = new LocalizeController(this);
@query('[part~="base"]') base: HTMLElement;
/**
* Indicates whether or not the alert is open. You can toggle this attribute to show and hide the alert, or you can
* use the `show()` and `hide()` methods and this attribute will reflect the alert's open state.
*/
@property({ type: Boolean, reflect: true }) open = false;
/** Enables a close button that allows the user to dismiss the alert. */
@property({ type: Boolean, reflect: true }) closable = false;
/** The alert's theme variant. */
@property({ reflect: true }) variant: 'primary' | 'success' | 'neutral' | 'warning' | 'danger' = 'primary';
/**
* The length of time, in milliseconds, the alert will show before closing itself. If the user interacts with
* the alert before it closes (e.g. moves the mouse over it), the timer will restart. Defaults to `Infinity`, meaning
* the alert will not close on its own.
*/
@property({ type: Number }) duration = Infinity;
firstUpdated() {
this.base.hidden = !this.open;
}
private restartAutoHide() {
clearTimeout(this.autoHideTimeout);
if (this.open && this.duration < Infinity) {
this.autoHideTimeout = window.setTimeout(() => this.hide(), this.duration);
}
}
private handleCloseClick() {
this.hide();
}
private handleMouseMove() {
this.restartAutoHide();
}
@watch('open', { waitUntilFirstUpdate: true })
async handleOpenChange() {
if (this.open) {
// Show
this.emit('sl-show');
if (this.duration < Infinity) {
this.restartAutoHide();
}
await stopAnimations(this.base);
this.base.hidden = false;
const { keyframes, options } = getAnimation(this, 'alert.show', { dir: this.localize.dir() });
await animateTo(this.base, keyframes, options);
this.emit('sl-after-show');
} else {
// Hide
this.emit('sl-hide');
clearTimeout(this.autoHideTimeout);
await stopAnimations(this.base);
const { keyframes, options } = getAnimation(this, 'alert.hide', { dir: this.localize.dir() });
await animateTo(this.base, keyframes, options);
this.base.hidden = true;
this.emit('sl-after-hide');
}
}
@watch('duration')
handleDurationChange() {
this.restartAutoHide();
}
/** Shows the alert. */
async show() {
if (this.open) {
return undefined;
}
this.open = true;
return waitForEvent(this, 'sl-after-show');
}
/** Hides the alert */
async hide() {
if (!this.open) {
return undefined;
}
this.open = false;
return waitForEvent(this, 'sl-after-hide');
}
/**
* Displays the alert as a toast notification. This will move the alert out of its position in the DOM and, when
* dismissed, it will be removed from the DOM completely. By storing a reference to the alert, you can reuse it by
* calling this method again. The returned promise will resolve after the alert is hidden.
*/
async toast() {
return new Promise<void>(resolve => {
if (toastStack.parentElement === null) {
document.body.append(toastStack);
}
toastStack.appendChild(this);
// Wait for the toast stack to render
requestAnimationFrame(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions -- force a reflow for the initial transition
this.clientWidth;
this.show();
});
this.addEventListener(
'sl-after-hide',
() => {
toastStack.removeChild(this);
resolve();
// Remove the toast stack from the DOM when there are no more alerts
if (toastStack.querySelector('sl-alert') === null) {
toastStack.remove();
}
},
{ once: true }
);
});
}
render() {
return html`
<div
part="base"
class=${classMap({
alert: true,
'alert--open': this.open,
'alert--closable': this.closable,
'alert--has-icon': this.hasSlotController.test('icon'),
'alert--primary': this.variant === 'primary',
'alert--success': this.variant === 'success',
'alert--neutral': this.variant === 'neutral',
'alert--warning': this.variant === 'warning',
'alert--danger': this.variant === 'danger'
})}
role="alert"
aria-hidden=${this.open ? 'false' : 'true'}
@mousemove=${this.handleMouseMove}
>
<slot name="icon" part="icon" class="alert__icon"></slot>
<slot part="message" class="alert__message" aria-live="polite"></slot>
${this.closable
? html`
<sl-icon-button
part="close-button"
exportparts="base:close-button__base"
class="alert__close-button"
name="x-lg"
library="system"
label=${this.localize.term('close')}
@click=${this.handleCloseClick}
></sl-icon-button>
`
: ''}
</div>
`;
}
}
setDefaultAnimation('alert.show', {
keyframes: [
{ opacity: 0, scale: 0.8 },
{ opacity: 1, scale: 1 }
],
options: { duration: 250, easing: 'ease' }
});
setDefaultAnimation('alert.hide', {
keyframes: [
{ opacity: 1, scale: 1 },
{ opacity: 0, scale: 0.8 }
],
options: { duration: 250, easing: 'ease' }
});
SlAlert.define('sl-alert');
declare global {
interface HTMLElementTagNameMap {

View File

@@ -0,0 +1,116 @@
import { html } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { watch } from '../../internal/watch.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import SlIcon from '../icon/icon.component.js';
import styles from './animated-image.styles.js';
import type { CSSResultGroup } from 'lit';
/**
* @summary A component for displaying animated GIFs and WEBPs that play and pause on interaction.
* @documentation https://shoelace.style/components/animated-image
* @status stable
* @since 2.0
*
* @dependency sl-icon
*
* @event sl-load - Emitted when the image loads successfully.
* @event sl-error - Emitted when the image fails to load.
*
* @slot play-icon - Optional play icon to use instead of the default. Works best with `<sl-icon>`.
* @slot pause-icon - Optional pause icon to use instead of the default. Works best with `<sl-icon>`.
*
* @part - control-box - The container that surrounds the pause/play icons and provides their background.
*
* @cssproperty --control-box-size - The size of the icon box.
* @cssproperty --icon-size - The size of the play/pause icons.
*/
export default class SlAnimatedImage extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static dependencies = { 'sl-icon': SlIcon };
@query('.animated-image__animated') animatedImage: HTMLImageElement;
@state() frozenFrame: string;
@state() isLoaded = false;
/** The path to the image to load. */
@property() src: string;
/** A description of the image used by assistive devices. */
@property() alt: string;
/** Plays the animation. When this attribute is remove, the animation will pause. */
@property({ type: Boolean, reflect: true }) play: boolean;
private handleClick() {
this.play = !this.play;
}
private 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) {
this.emit('sl-load');
this.isLoaded = true;
}
}
private handleError() {
this.emit('sl-error');
}
@watch('play', { waitUntilFirstUpdate: true })
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">
<slot name="play-icon"><sl-icon name="play-fill" library="system"></sl-icon></slot>
<slot name="pause-icon"><sl-icon name="pause-fill" library="system"></sl-icon></slot>
</div>
`
: ''}
</div>
`;
}
}

View File

@@ -1,119 +1,9 @@
import '../icon/icon.js';
import { customElement, property, query, state } from 'lit/decorators.js';
import { html } from 'lit';
import { watch } from '../../internal/watch.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import styles from './animated-image.styles.js';
import type { CSSResultGroup } from 'lit';
import SlAnimatedImage from './animated-image.component.js';
/**
* @summary A component for displaying animated GIFs and WEBPs that play and pause on interaction.
* @documentation https://shoelace.style/components/animated-image
* @status stable
* @since 2.0
*
* @dependency sl-icon
*
* @event sl-load - Emitted when the image loads successfully.
* @event sl-error - Emitted when the image fails to load.
*
* @slot play-icon - Optional play icon to use instead of the default. Works best with `<sl-icon>`.
* @slot pause-icon - Optional pause icon to use instead of the default. Works best with `<sl-icon>`.
*
* @part - control-box - The container that surrounds the pause/play icons and provides their background.
*
* @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 ShoelaceElement {
static styles: CSSResultGroup = styles;
export * from './animated-image.component.js';
export default SlAnimatedImage;
@query('.animated-image__animated') animatedImage: HTMLImageElement;
@state() frozenFrame: string;
@state() isLoaded = false;
/** The path to the image to load. */
@property() src: string;
/** A description of the image used by assistive devices. */
@property() alt: string;
/** Plays the animation. When this attribute is remove, the animation will pause. */
@property({ type: Boolean, reflect: true }) play: boolean;
private handleClick() {
this.play = !this.play;
}
private 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) {
this.emit('sl-load');
this.isLoaded = true;
}
}
private handleError() {
this.emit('sl-error');
}
@watch('play', { waitUntilFirstUpdate: true })
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">
<slot name="play-icon"><sl-icon name="play-fill" library="system"></sl-icon></slot>
<slot name="pause-icon"><sl-icon name="pause-fill" library="system"></sl-icon></slot>
</div>
`
: ''}
</div>
`;
}
}
SlAnimatedImage.define('sl-animated-image');
declare global {
interface HTMLElementTagNameMap {

View File

@@ -0,0 +1,220 @@
import { animations } from './animations.js';
import { html } from 'lit';
import { property, queryAsync } from 'lit/decorators.js';
import { watch } from '../../internal/watch.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import styles from './animation.styles.js';
import type { CSSResultGroup } from 'lit';
/**
* @summary Animate elements declaratively with nearly 100 baked-in presets, or roll your own with custom keyframes. Powered by the [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API).
* @documentation https://shoelace.style/components/animation
* @status stable
* @since 2.0
*
* @event sl-cancel - Emitted when the animation is canceled.
* @event sl-finish - Emitted when the animation finishes.
* @event sl-start - Emitted when the animation starts or restarts.
*
* @slot - The element to animate. Avoid slotting in more than one element, as subsequent ones will be ignored. To
* animate multiple elements, either wrap them in a single container or use multiple `<sl-animation>` elements.
*/
export default class SlAnimation extends ShoelaceElement {
static styles: CSSResultGroup = styles;
private animation?: Animation;
private hasStarted = false;
@queryAsync('slot') defaultSlot: Promise<HTMLSlotElement>;
/** The name of the built-in animation to use. For custom animations, use the `keyframes` prop. */
@property() name = 'none';
/**
* Plays the animation. When omitted, the animation will be paused. This attribute will be automatically removed when
* the animation finishes or gets canceled.
*/
@property({ type: Boolean, reflect: true }) play = false;
/** The number of milliseconds to delay the start of the animation. */
@property({ type: Number }) delay = 0;
/**
* Determines the direction of playback as well as the behavior when reaching the end of an iteration.
* [Learn more](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-direction)
*/
@property() direction: PlaybackDirection = 'normal';
/** The number of milliseconds each iteration of the animation takes to complete. */
@property({ type: Number }) duration = 1000;
/**
* The easing function to use for the animation. This can be a Shoelace easing function or a custom easing function
* such as `cubic-bezier(0, 1, .76, 1.14)`.
*/
@property() easing = 'linear';
/** The number of milliseconds to delay after the active period of an animation sequence. */
@property({ attribute: 'end-delay', type: Number }) endDelay = 0;
/** Sets how the animation applies styles to its target before and after its execution. */
@property() fill: FillMode = 'auto';
/** The number of iterations to run before the animation completes. Defaults to `Infinity`, which loops. */
@property({ type: Number }) iterations = Infinity;
/** The offset at which to start the animation, usually between 0 (start) and 1 (end). */
@property({ attribute: 'iteration-start', type: Number }) iterationStart = 0;
/** The keyframes to use for the animation. If this is set, `name` will be ignored. */
@property({ attribute: false }) keyframes?: Keyframe[];
/**
* Sets the animation's playback rate. The default is `1`, which plays the animation at a normal speed. Setting this
* to `2`, for example, will double the animation's speed. A negative value can be used to reverse the animation. This
* value can be changed without causing the animation to restart.
*/
@property({ attribute: 'playback-rate', type: Number }) playbackRate = 1;
/** Gets and sets the current animation time. */
get currentTime(): CSSNumberish {
return this.animation?.currentTime ?? 0;
}
set currentTime(time: number) {
if (this.animation) {
this.animation.currentTime = time;
}
}
connectedCallback() {
super.connectedCallback();
this.createAnimation();
}
disconnectedCallback() {
super.disconnectedCallback();
this.destroyAnimation();
}
private handleAnimationFinish = () => {
this.play = false;
this.hasStarted = false;
this.emit('sl-finish');
};
private handleAnimationCancel = () => {
this.play = false;
this.hasStarted = false;
this.emit('sl-cancel');
};
private handleSlotChange() {
this.destroyAnimation();
this.createAnimation();
}
private async createAnimation() {
const easing = animations.easings[this.easing] ?? this.easing;
const keyframes = this.keyframes ?? (animations as unknown as Partial<Record<string, Keyframe[]>>)[this.name];
const slot = await this.defaultSlot;
const element = slot.assignedElements()[0] as HTMLElement | undefined;
if (!element || !keyframes) {
return false;
}
this.destroyAnimation();
this.animation = element.animate(keyframes, {
delay: this.delay,
direction: this.direction,
duration: this.duration,
easing,
endDelay: this.endDelay,
fill: this.fill,
iterationStart: this.iterationStart,
iterations: this.iterations
});
this.animation.playbackRate = this.playbackRate;
this.animation.addEventListener('cancel', this.handleAnimationCancel);
this.animation.addEventListener('finish', this.handleAnimationFinish);
if (this.play) {
this.hasStarted = true;
this.emit('sl-start');
} else {
this.animation.pause();
}
return true;
}
private destroyAnimation() {
if (this.animation) {
this.animation.cancel();
this.animation.removeEventListener('cancel', this.handleAnimationCancel);
this.animation.removeEventListener('finish', this.handleAnimationFinish);
this.hasStarted = false;
}
}
@watch([
'name',
'delay',
'direction',
'duration',
'easing',
'endDelay',
'fill',
'iterations',
'iterationsStart',
'keyframes'
])
handleAnimationChange() {
if (!this.hasUpdated) {
return;
}
this.createAnimation();
}
@watch('play')
handlePlayChange() {
if (this.animation) {
if (this.play && !this.hasStarted) {
this.hasStarted = true;
this.emit('sl-start');
}
if (this.play) {
this.animation.play();
} else {
this.animation.pause();
}
return true;
}
return false;
}
@watch('playbackRate')
handlePlaybackRateChange() {
if (this.animation) {
this.animation.playbackRate = this.playbackRate;
}
}
/** Clears all keyframe effects caused by this animation and aborts its playback. */
cancel() {
this.animation?.cancel();
}
/** Sets the playback time to the end of the animation corresponding to the current playback direction. */
finish() {
this.animation?.finish();
}
render() {
return html` <slot @slotchange=${this.handleSlotChange}></slot> `;
}
}

View File

@@ -1,224 +1,9 @@
import { animations } from './animations.js';
import { customElement, property, queryAsync } from 'lit/decorators.js';
import { html } from 'lit';
import { watch } from '../../internal/watch.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import styles from './animation.styles.js';
import type { CSSResultGroup } from 'lit';
import SlAnimation from './animation.component.js';
/**
* @summary Animate elements declaratively with nearly 100 baked-in presets, or roll your own with custom keyframes. Powered by the [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API).
* @documentation https://shoelace.style/components/animation
* @status stable
* @since 2.0
*
* @event sl-cancel - Emitted when the animation is canceled.
* @event sl-finish - Emitted when the animation finishes.
* @event sl-start - Emitted when the animation starts or restarts.
*
* @slot - The element to animate. Avoid slotting in more than one element, as subsequent ones will be ignored. To
* animate multiple elements, either wrap them in a single container or use multiple `<sl-animation>` elements.
*/
@customElement('sl-animation')
export default class SlAnimation extends ShoelaceElement {
static styles: CSSResultGroup = styles;
export * from './animation.component.js';
export default SlAnimation;
private animation?: Animation;
private hasStarted = false;
@queryAsync('slot') defaultSlot: Promise<HTMLSlotElement>;
/** The name of the built-in animation to use. For custom animations, use the `keyframes` prop. */
@property() name = 'none';
/**
* Plays the animation. When omitted, the animation will be paused. This attribute will be automatically removed when
* the animation finishes or gets canceled.
*/
@property({ type: Boolean, reflect: true }) play = false;
/** The number of milliseconds to delay the start of the animation. */
@property({ type: Number }) delay = 0;
/**
* Determines the direction of playback as well as the behavior when reaching the end of an iteration.
* [Learn more](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-direction)
*/
@property() direction: PlaybackDirection = 'normal';
/** The number of milliseconds each iteration of the animation takes to complete. */
@property({ type: Number }) duration = 1000;
/**
* The easing function to use for the animation. This can be a Shoelace easing function or a custom easing function
* such as `cubic-bezier(0, 1, .76, 1.14)`.
*/
@property() easing = 'linear';
/** The number of milliseconds to delay after the active period of an animation sequence. */
@property({ attribute: 'end-delay', type: Number }) endDelay = 0;
/** Sets how the animation applies styles to its target before and after its execution. */
@property() fill: FillMode = 'auto';
/** The number of iterations to run before the animation completes. Defaults to `Infinity`, which loops. */
@property({ type: Number }) iterations = Infinity;
/** The offset at which to start the animation, usually between 0 (start) and 1 (end). */
@property({ attribute: 'iteration-start', type: Number }) iterationStart = 0;
/** The keyframes to use for the animation. If this is set, `name` will be ignored. */
@property({ attribute: false }) keyframes?: Keyframe[];
/**
* Sets the animation's playback rate. The default is `1`, which plays the animation at a normal speed. Setting this
* to `2`, for example, will double the animation's speed. A negative value can be used to reverse the animation. This
* value can be changed without causing the animation to restart.
*/
@property({ attribute: 'playback-rate', type: Number }) playbackRate = 1;
/** Gets and sets the current animation time. */
get currentTime(): CSSNumberish {
return this.animation?.currentTime ?? 0;
}
set currentTime(time: number) {
if (this.animation) {
this.animation.currentTime = time;
}
}
connectedCallback() {
super.connectedCallback();
this.createAnimation();
}
disconnectedCallback() {
super.disconnectedCallback();
this.destroyAnimation();
}
private handleAnimationFinish = () => {
this.play = false;
this.hasStarted = false;
this.emit('sl-finish');
};
private handleAnimationCancel = () => {
this.play = false;
this.hasStarted = false;
this.emit('sl-cancel');
};
private handleSlotChange() {
this.destroyAnimation();
this.createAnimation();
}
private async createAnimation() {
const easing = animations.easings[this.easing] ?? this.easing;
const keyframes = this.keyframes ?? (animations as unknown as Partial<Record<string, Keyframe[]>>)[this.name];
const slot = await this.defaultSlot;
const element = slot.assignedElements()[0] as HTMLElement | undefined;
if (!element || !keyframes) {
return false;
}
this.destroyAnimation();
this.animation = element.animate(keyframes, {
delay: this.delay,
direction: this.direction,
duration: this.duration,
easing,
endDelay: this.endDelay,
fill: this.fill,
iterationStart: this.iterationStart,
iterations: this.iterations
});
this.animation.playbackRate = this.playbackRate;
this.animation.addEventListener('cancel', this.handleAnimationCancel);
this.animation.addEventListener('finish', this.handleAnimationFinish);
if (this.play) {
this.hasStarted = true;
this.emit('sl-start');
} else {
this.animation.pause();
}
return true;
}
private destroyAnimation() {
if (this.animation) {
this.animation.cancel();
this.animation.removeEventListener('cancel', this.handleAnimationCancel);
this.animation.removeEventListener('finish', this.handleAnimationFinish);
this.hasStarted = false;
}
}
@watch([
'name',
'delay',
'direction',
'duration',
'easing',
'endDelay',
'fill',
'iterations',
'iterationsStart',
'keyframes'
])
handleAnimationChange() {
if (!this.hasUpdated) {
return;
}
this.createAnimation();
}
@watch('play')
handlePlayChange() {
if (this.animation) {
if (this.play && !this.hasStarted) {
this.hasStarted = true;
this.emit('sl-start');
}
if (this.play) {
this.animation.play();
} else {
this.animation.pause();
}
return true;
}
return false;
}
@watch('playbackRate')
handlePlaybackRateChange() {
if (this.animation) {
this.animation.playbackRate = this.playbackRate;
}
}
/** Clears all keyframe effects caused by this animation and aborts its playback. */
cancel() {
this.animation?.cancel();
}
/** Sets the playback time to the end of the animation corresponding to the current playback direction. */
finish() {
this.animation?.finish();
}
render() {
return html` <slot @slotchange=${this.handleSlotChange}></slot> `;
}
}
SlAnimation.define('sl-animation');
declare global {
interface HTMLElementTagNameMap {

View File

@@ -0,0 +1,98 @@
import { classMap } from 'lit/directives/class-map.js';
import { html } from 'lit';
import { property, state } from 'lit/decorators.js';
import { watch } from '../../internal/watch.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import SlIcon from '../icon/icon.component.js';
import styles from './avatar.styles.js';
import type { CSSResultGroup } from 'lit';
/**
* @summary Avatars are used to represent a person or object.
* @documentation https://shoelace.style/components/avatar
* @status stable
* @since 2.0
*
* @dependency sl-icon
*
* @slot icon - The default icon to use when no image or initials are present. Works best with `<sl-icon>`.
*
* @csspart base - The component's base wrapper.
* @csspart icon - The container that wraps the avatar's icon.
* @csspart initials - The container that wraps the avatar's initials.
* @csspart image - The avatar image. Only shown when the `image` attribute is set.
*
* @cssproperty --size - The size of the avatar.
*/
export default class SlAvatar extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static dependencies = {
'sl-icon': SlIcon
};
@state() private hasError = false;
/** The image source to use for the avatar. */
@property() image = '';
/** A label to use to describe the avatar to assistive devices. */
@property() label = '';
/** Initials to use as a fallback when no image is available (1-2 characters max recommended). */
@property() initials = '';
/** Indicates how the browser should load the image. */
@property() loading: 'eager' | 'lazy' = 'eager';
/** The shape of the avatar. */
@property({ reflect: true }) shape: 'circle' | 'square' | 'rounded' = 'circle';
@watch('image')
handleImageChange() {
// Reset the error when a new image is provided
this.hasError = false;
}
render() {
const avatarWithImage = html`
<img
part="image"
class="avatar__image"
src="${this.image}"
loading="${this.loading}"
alt=""
@error="${() => (this.hasError = true)}"
/>
`;
let avatarWithoutImage = html``;
if (this.initials) {
avatarWithoutImage = html`<div part="initials" class="avatar__initials">${this.initials}</div>`;
} else {
avatarWithoutImage = html`
<div part="icon" class="avatar__icon" aria-hidden="true">
<slot name="icon">
<sl-icon name="person-fill" library="system"></sl-icon>
</slot>
</div>
`;
}
return html`
<div
part="base"
class=${classMap({
avatar: true,
'avatar--circle': this.shape === 'circle',
'avatar--rounded': this.shape === 'rounded',
'avatar--square': this.shape === 'square'
})}
role="img"
aria-label=${this.label}
>
${this.image && !this.hasError ? avatarWithImage : avatarWithoutImage}
</div>
`;
}
}

View File

@@ -1,97 +1,9 @@
import '../icon/icon.js';
import { classMap } from 'lit/directives/class-map.js';
import { customElement, property, state } from 'lit/decorators.js';
import { html } from 'lit';
import { watch } from '../../internal/watch.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import styles from './avatar.styles.js';
import type { CSSResultGroup } from 'lit';
import SlAvatar from './avatar.component.js';
/**
* @summary Avatars are used to represent a person or object.
* @documentation https://shoelace.style/components/avatar
* @status stable
* @since 2.0
*
* @dependency sl-icon
*
* @slot icon - The default icon to use when no image or initials are present. Works best with `<sl-icon>`.
*
* @csspart base - The component's base wrapper.
* @csspart icon - The container that wraps the avatar's icon.
* @csspart initials - The container that wraps the avatar's initials.
* @csspart image - The avatar image. Only shown when the `image` attribute is set.
*
* @cssproperty --size - The size of the avatar.
*/
@customElement('sl-avatar')
export default class SlAvatar extends ShoelaceElement {
static styles: CSSResultGroup = styles;
export * from './avatar.component.js';
export default SlAvatar;
@state() private hasError = false;
/** The image source to use for the avatar. */
@property() image = '';
/** A label to use to describe the avatar to assistive devices. */
@property() label = '';
/** Initials to use as a fallback when no image is available (1-2 characters max recommended). */
@property() initials = '';
/** Indicates how the browser should load the image. */
@property() loading: 'eager' | 'lazy' = 'eager';
/** The shape of the avatar. */
@property({ reflect: true }) shape: 'circle' | 'square' | 'rounded' = 'circle';
@watch('image')
handleImageChange() {
// Reset the error when a new image is provided
this.hasError = false;
}
render() {
const avatarWithImage = html`
<img
part="image"
class="avatar__image"
src="${this.image}"
loading="${this.loading}"
alt=""
@error="${() => (this.hasError = true)}"
/>
`;
let avatarWithoutImage = html``;
if (this.initials) {
avatarWithoutImage = html`<div part="initials" class="avatar__initials">${this.initials}</div>`;
} else {
avatarWithoutImage = html`
<slot name="icon" part="icon" class="avatar__icon" aria-hidden="true">
<sl-icon name="person-fill" library="system"></sl-icon>
</slot>
`;
}
return html`
<div
part="base"
class=${classMap({
avatar: true,
'avatar--circle': this.shape === 'circle',
'avatar--rounded': this.shape === 'rounded',
'avatar--square': this.shape === 'square'
})}
role="img"
aria-label=${this.label}
>
${this.image && !this.hasError ? avatarWithImage : avatarWithoutImage}
</div>
`;
}
}
SlAvatar.define('sl-avatar');
declare global {
interface HTMLElementTagNameMap {

View File

@@ -0,0 +1,50 @@
import { classMap } from 'lit/directives/class-map.js';
import { html } from 'lit';
import { property } from 'lit/decorators.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import styles from './badge.styles.js';
import type { CSSResultGroup } from 'lit';
/**
* @summary Badges are used to draw attention and display statuses or counts.
* @documentation https://shoelace.style/components/badge
* @status stable
* @since 2.0
*
* @slot - The badge's content.
*
* @csspart base - The component's base wrapper.
*/
export default class SlBadge extends ShoelaceElement {
static styles: CSSResultGroup = styles;
/** The badge's theme variant. */
@property({ reflect: true }) variant: 'primary' | 'success' | 'neutral' | 'warning' | 'danger' = 'primary';
/** Draws a pill-style badge with rounded edges. */
@property({ type: Boolean, reflect: true }) pill = false;
/** Makes the badge pulsate to draw attention. */
@property({ type: Boolean, reflect: true }) pulse = false;
render() {
return html`
<span
part="base"
class=${classMap({
badge: true,
'badge--primary': this.variant === 'primary',
'badge--success': this.variant === 'success',
'badge--neutral': this.variant === 'neutral',
'badge--warning': this.variant === 'warning',
'badge--danger': this.variant === 'danger',
'badge--pill': this.pill,
'badge--pulse': this.pulse
})}
role="status"
>
<slot></slot>
</span>
`;
}
}

View File

@@ -2,6 +2,10 @@ import '../../../dist/shoelace.js';
import { expect, fixture, html } from '@open-wc/testing';
import type SlBadge from './badge.js';
// The default badge background just misses AA contrast, but the next step up is way too dark. We're going to relax this
// rule for now.
const ignoredRules = ['color-contrast'];
describe('<sl-badge>', () => {
let el: SlBadge;
@@ -11,7 +15,7 @@ describe('<sl-badge>', () => {
});
it('should pass accessibility tests with a role of status on the base part.', async () => {
await expect(el).to.be.accessible();
await expect(el).to.be.accessible({ ignoredRules });
const part = el.shadowRoot!.querySelector('[part~="base"]')!;
expect(part.getAttribute('role')).to.eq('status');
@@ -33,7 +37,7 @@ describe('<sl-badge>', () => {
});
it('should pass accessibility tests', async () => {
await expect(el).to.be.accessible();
await expect(el).to.be.accessible({ ignoredRules });
});
it('should append the pill class to the classlist to render a pill', () => {
@@ -48,7 +52,7 @@ describe('<sl-badge>', () => {
});
it('should pass accessibility tests', async () => {
await expect(el).to.be.accessible();
await expect(el).to.be.accessible({ ignoredRules });
});
it('should append the pulse class to the classlist to render a pulse', () => {
@@ -64,7 +68,7 @@ describe('<sl-badge>', () => {
});
it('should pass accessibility tests', async () => {
await expect(el).to.be.accessible();
await expect(el).to.be.accessible({ ignoredRules });
});
it('should default to square styling, with the primary color', () => {

Some files were not shown because too many files have changed in this diff Show More