Compare commits

...

197 Commits

Author SHA1 Message Date
Cory LaViska
8ee519d40a fix positioning 2021-10-30 15:54:56 -04:00
Cory LaViska
6bc17d48c3 update examples 2021-10-29 18:35:24 -04:00
Cory LaViska
a1263f1b9d add experimental context menu 2021-10-29 18:32:45 -04:00
Cory LaViska
d69ebab765 update examples 2021-10-29 18:32:26 -04:00
Cory LaViska
0504946dac refactor popper creation 2021-10-29 18:32:12 -04:00
Cory LaViska
fbd6691711 improve search panel color 2021-10-29 14:42:17 -04:00
Cory LaViska
aec17da6b0 improve trigger border color in dark mode 2021-10-29 14:35:57 -04:00
Cory LaViska
639533662d update changelog 2021-10-26 09:35:59 -04:00
Cory LaViska
a340ce4a68 document part 2021-10-26 09:35:46 -04:00
Cory LaViska
6e5fe64e8b add eye dropper 2021-10-26 09:35:07 -04:00
Cory LaViska
84bdbb84b8 update lit 2021-10-26 09:34:48 -04:00
Cory LaViska
f91ffb6cb4 fix border radius on single button groups 2021-10-26 09:34:33 -04:00
Cory LaViska
13815199a3 fix dark theme link 2021-10-22 10:57:07 -04:00
Cory LaViska
98c20ff551 2.0.0-beta.58 2021-10-22 10:52:27 -04:00
Cory LaViska
479b6b9081 bundle back up for now 2021-10-22 10:51:17 -04:00
Cory LaViska
c640d2ea77 add stack overflow section 2021-10-19 10:51:41 -04:00
Cory LaViska
715547d2fd update changelog 2021-10-19 09:56:41 -04:00
Cory LaViska
8a914a536b fix cssproperty docs 2021-10-19 09:56:28 -04:00
Cory LaViska
f56b6c0648 remove RAFs 2021-10-19 09:52:41 -04:00
Denis Korablev
25aa8318d9 fix(sl-range): add value change handler (#572) 2021-10-19 09:48:39 -04:00
Cory LaViska
72f2cbe9e8 fix aspect ratio bug 2021-10-19 09:43:16 -04:00
Cory LaViska
fc7836084a add tooltip guard 2021-10-18 17:54:29 -04:00
Cory LaViska
60d9d9202b update bootstrap-icons to 1.6.1 2021-10-18 17:07:52 -04:00
Cory LaViska
a9df468282 fixes #563 2021-10-18 17:07:07 -04:00
Yuki Nishijima
0bba773c3e Bring the divider back to the Shadow DOM (#568) 2021-10-18 09:14:13 -04:00
Cory LaViska
7be03ae623 fix metadata plugin 2021-10-18 08:58:50 -04:00
Cory LaViska
d4741532f5 fix build dir 2021-10-16 10:35:42 -04:00
Cory LaViska
10f31efefa fix comment parser 2021-10-16 10:28:51 -04:00
Cory LaViska
be662ddf32 add animated-image 2021-10-16 08:29:25 -04:00
Cory LaViska
ff84beaade use :enabled 2021-10-14 09:02:00 -04:00
Cory LaViska
8dba8fa5fb fix tooltip bug 2021-10-14 09:01:37 -04:00
Cory LaViska
3a3f5552a7 fix test:watch 2021-10-14 08:39:17 -04:00
Cory LaViska
88cba353c0 add labels examples 2021-10-14 08:34:54 -04:00
Cory LaViska
a2851370bb revert styles 2021-10-14 08:32:11 -04:00
Cory LaViska
7c0ef7dcf0 Merge branch 'christoshrousis-test/progress' into next 2021-10-14 08:25:42 -04:00
Cory LaViska
fb6d5d89b8 use label attrib 2021-10-14 08:24:38 -04:00
Cory LaViska
45ceff4c08 Merge branch 'test/progress' of https://github.com/christoshrousis/shoelace into christoshrousis-test/progress 2021-10-14 08:10:57 -04:00
Cory LaViska
6169abc700 update bootstrap icons 2021-10-14 07:21:27 -04:00
Cory LaViska
c09e12d13e 2.0.0-beta.57 2021-10-13 17:34:04 -04:00
Cory LaViska
6152e15e10 fix esm links 2021-10-13 17:30:13 -04:00
Cory LaViska
79910b2ae8 2.0.0-beta.56 2021-10-13 17:14:22 -04:00
Cory LaViska
c347df7c17 rework build to support bare specifiers 2021-10-13 17:12:50 -04:00
Cory LaViska
e9e2b35c59 Merge branch 'next' of https://github.com/shoelace-style/shoelace into next 2021-10-13 08:30:16 -04:00
Denis Korablev
8ae753c396 feat: add hoist attribute for sl-tooltip (#565) 2021-10-13 08:29:38 -04:00
Cory LaViska
d2c94321f2 update docs 2021-10-12 10:26:26 -04:00
Yuki Nishijima
4c10f8a537 Update the CSS path in the Integrating with Rails (#561) 2021-10-11 08:25:09 -04:00
Christos Hrousis
9a19cc2173 revert: misunderstood part/slot definition. 2021-10-10 13:37:32 +11:00
Christos Hrousis
c4cbc894f5 revert: misunderstood part/slot definition. 2021-10-10 13:36:41 +11:00
Christos Hrousis
449f5e6c7f style: typo. 2021-10-10 13:24:28 +11:00
Christos Hrousis
34447a3f2f test: migrate progress-ring tests to progress-bar
- Match coverage with progress-ring
- Attached titles/label/labelledby
- Value '' on aria-valuenow is does not pass AXE
2021-10-10 13:24:01 +11:00
Christos Hrousis
eee97d7dba test: cover progress-ring
- Add title to make ring accessibly hoverable.
- Add label/labelledby as aria options.
- Remove ununsed label slot.
2021-10-10 13:05:40 +11:00
Christos Hrousis
f16392947a docs: ring uses css prop for track-width. 2021-10-10 13:00:41 +11:00
Cory LaViska
c3adf92b49 2.0.0-beta.55 2021-10-08 17:06:12 -04:00
Cory LaViska
a6580b018d updat3e changelog 2021-10-08 17:05:53 -04:00
Cory LaViska
00c843c7ce revert unbundling 2021-10-08 17:04:30 -04:00
Cory LaViska
5fe55a4db9 2.0.0-beta.54 2021-10-08 16:55:09 -04:00
Cory LaViska
4ca998c346 revert 2021-10-08 16:54:57 -04:00
Cory LaViska
42b3e2cc11 update changelog 2021-10-08 16:53:31 -04:00
Cory LaViska
59fb8db6be unbundle select deps 2021-10-08 16:52:01 -04:00
Cory LaViska
664beafefa 2.0.0-beta.54 2021-10-08 16:42:53 -04:00
Cory LaViska
04443a64e2 update changelog 2021-10-08 16:39:10 -04:00
Cory LaViska
92dedf3386 ship bare module specifiers for prod 2021-10-08 10:11:12 -04:00
Cory LaViska
2ba5fb9820 update command line args 2021-10-08 10:10:26 -04:00
Cory LaViska
222235159b rename clear => remove 2021-10-07 09:52:23 -04:00
Cory LaViska
1061bd5e0d add disabled prop 2021-10-07 09:31:20 -04:00
Cory LaViska
ccec9a8348 fix initial disabled state 2021-10-07 09:30:47 -04:00
Cory LaViska
bf9e06e67d remove attr column 2021-10-05 10:41:52 -04:00
Cory LaViska
1e03d222c5 fix code in tables 2021-10-05 10:41:39 -04:00
Cory LaViska
3722f46b8e Merge branch 'kanoni4567-next' into next 2021-10-05 09:03:51 -04:00
Cory LaViska
8d7bf97127 Merge branch 'next' of https://github.com/kanoni4567/shoelace into kanoni4567-next 2021-10-05 09:01:35 -04:00
Cory LaViska
d26c1a6407 update docs 2021-10-05 08:58:41 -04:00
Rich Klein
f296ff8476 Create integrating-with-laravel.md (#553)
* Create integrating-with-laravel.md

Instructions for using Shoelace components in a Laravel 8.x application.

* Update integrating-with-laravel.md

Added a section for setting the base path and switched to using the full import path for each component. Also included a full `bootstrap.js` example.
2021-10-05 08:54:12 -04:00
Christos Hrousis
a75a71994a test: component/spinner (#556)
- covers accessibility
- provides explainer for aria-busy and aria-live tags.
2021-10-05 08:53:28 -04:00
Shanyu Cui
7d1373a1d1 radio: emit sl-change when toggled via arrow keys 2021-10-04 23:27:28 -07:00
Shanyu Cui
23abc50015 Merge branch 'shoelace-style:next' into next 2021-10-04 22:39:37 -07:00
Cory LaViska
5e28d1131a 2.0.0-beta.53 2021-10-04 09:31:07 -04:00
Cory LaViska
a141e64a69 update version 2021-10-04 09:30:21 -04:00
Cory LaViska
e12ee97bd9 add --padding to tab panel 2021-10-04 09:29:14 -04:00
Cory LaViska
ebd84642e1 Merge branch 'christoshrousis-test/card' into next 2021-10-04 09:06:55 -04:00
Christos Hrousis
888ac2ea0d test: first pass test.
- Cover use of header slot.
- Cover use of footer slot.
- Cover use of image slot.
2021-10-02 19:44:45 +10:00
Cory LaViska
37068c922c add margin 2021-10-01 09:03:29 -04:00
Cory LaViska
eec24d2ed1 add sl-mutation-observer 2021-09-30 18:32:59 -04:00
Cory LaViska
8fa9d629a3 fix tag default 2021-09-30 17:42:34 -04:00
Cory LaViska
ef22dd7dc4 fix docs 2021-09-30 17:16:27 -04:00
Cory LaViska
3f12624b78 bye, dotty! 2021-09-30 11:20:05 -04:00
Cory LaViska
d6fa67374c revert table change 2021-09-30 09:37:39 -04:00
Cory LaViska
d4183cf718 fix indentations 2021-09-30 09:28:00 -04:00
Cory LaViska
4ad0480039 rename percentage => value 2021-09-30 09:02:28 -04:00
Cory LaViska
7188425ac0 fix animation bug 2021-09-30 08:40:21 -04:00
Cory LaViska
c83581cf47 improve search 2021-09-30 08:07:15 -04:00
Cory LaViska
f8fa29f157 Merge branch 'timefordroids-550' into next 2021-09-30 07:51:54 -04:00
Cory LaViska
f2a4db6291 update docs, expose custom properties, rework to not use getComputedStyle 2021-09-30 07:51:28 -04:00
Denis
a4aff0b1e9 #550 added progress indicator to sl-range component 2021-09-30 11:20:39 +04:00
Cory LaViska
f5c2e0b425 fixes #549 2021-09-29 17:03:37 -04:00
Cory LaViska
2f88c55ec0 fix default height 2021-09-29 09:40:56 -04:00
Cory LaViska
db7075a91a update changelog 2021-09-29 08:42:39 -04:00
Narendra Sisodiya
8ac007ba9a Removed lit-html and added lit (#547) 2021-09-29 08:40:26 -04:00
Cory LaViska
111fa8397c fix default outline border 2021-09-29 08:38:43 -04:00
Cory LaViska
0ef4e92a96 add border 2021-09-29 08:35:43 -04:00
Cory LaViska
9123afac18 add examples 2021-09-29 08:33:52 -04:00
Cory LaViska
1b5dca52e3 add divider component 2021-09-29 08:03:03 -04:00
Christos Hrousis
bc3e9c43da test: components/breadcrumb-item (#541)
* test: migrate skipped tests to the breadcrumb-item

* test: when rendering a HTMLAnchorElement

* test: add checks for classes.

* test: uplift langauge and query selector to test label part

* test: default button test

* refactor: set default for rel on prop declaration.
2021-09-28 08:27:19 -04:00
Cory LaViska
ded01cfd8a fixes #544 2021-09-28 08:22:26 -04:00
Cory LaViska
c737b7494f fix comments 2021-09-27 17:58:14 -04:00
Cory LaViska
47aff56e71 add panel border width token 2021-09-27 16:53:24 -04:00
Cory LaViska
15ce341d81 add filled tokens and update styles 2021-09-27 10:48:47 -04:00
Cory LaViska
7476204258 add theme toggle shortcut 2021-09-27 10:24:51 -04:00
Cory LaViska
e083b1a02e revert luminance removal and rework surface tokens 2021-09-27 10:24:29 -04:00
Cory LaViska
fa74fc54e3 fix bg 2021-09-24 22:38:08 -04:00
Cory LaViska
fbbeec6d2f Merge branch 'christoshrousis-test/breadcrumb' into next 2021-09-24 22:31:21 -04:00
Cory LaViska
b4192364f6 Merge branch 'test/breadcrumb' of https://github.com/christoshrousis/shoelace into christoshrousis-test/breadcrumb 2021-09-24 22:30:16 -04:00
Cory LaViska
c7dc82947f add filled variant 2021-09-24 22:28:14 -04:00
Christos Hrousis
c38fd3986c test: uplift sepcificity of aria test, and test separator. 2021-09-25 12:12:50 +10:00
Christos Hrousis
35d7926e18 test: first pass test for breadcrumb 2021-09-25 11:41:00 +10:00
Christos Hrousis
86e06ce1e6 test: start the breadcrumb test. 2021-09-25 11:41:00 +10:00
Cory LaViska
1a08f063a6 add outline buttons; closes #522 2021-09-24 16:40:03 -04:00
Cory LaViska
740208ed76 Merge branch 'next' of https://github.com/shoelace-style/shoelace into next 2021-09-24 09:18:45 -04:00
Cory LaViska
521f6fe3f1 update test 2021-09-24 09:18:28 -04:00
Cory LaViska
f43d490763 Create node.js.yml 2021-09-24 09:07:06 -04:00
Christos Hrousis
898179b645 test: components/avatar (#529)
* test: introduce first pass test for components/avatar
feat: set alt="" on img element

* test: resolve img src 404 errors

* test: remove bad test

* test: test children by assigning nodes...

* test: add patches similar to badge patch

* style: address typing issues.
2021-09-24 08:56:08 -04:00
Cory LaViska
8c3888da02 add control part to select 2021-09-24 08:48:01 -04:00
Cory LaViska
e1471ec9a1 change default dropdown distance 2021-09-24 08:31:54 -04:00
Cory LaViska
eac07a51ba update issue templates 2021-09-23 09:32:53 -04:00
Cory LaViska
753000b56a update changelog 2021-09-23 08:54:04 -04:00
Cory LaViska
8ab2907c3f update components to use surface tokens 2021-09-23 08:53:05 -04:00
Cory LaViska
723ee80c8f update docs to use surface tokens 2021-09-23 08:52:49 -04:00
Cory LaViska
e1c003c8df add surface tokens and remove luminance shift 2021-09-23 08:52:18 -04:00
Cory LaViska
96c82dc69f fix discord badge 2021-09-23 07:54:14 -04:00
Cory LaViska
0dd9483797 remove bogus space 2021-09-22 15:35:26 -04:00
Cory LaViska
4da110087d update badges 2021-09-22 10:04:42 -04:00
Cory LaViska
7f75a64647 update vue usage 2021-09-22 09:06:13 -04:00
Cory LaViska
71d7f6fd98 2.0.0-beta.52 2021-09-20 23:22:29 -04:00
Cory LaViska
42595f241f fix test 2021-09-20 23:20:29 -04:00
Cory LaViska
c67c84dfda simplify math 2021-09-20 23:14:39 -04:00
Cory LaViska
2ba6a3b097 remove unused style 2021-09-20 22:33:17 -04:00
Cory LaViska
0456ad96fb update changelog 2021-09-20 22:09:14 -04:00
Cory LaViska
0ab87b03fa remove dashes from animation descriptions 2021-09-20 22:05:27 -04:00
Cory LaViska
54fc41e175 update to lit 2.0 2021-09-20 22:01:21 -04:00
Cory LaViska
261e824290 update changelog 2021-09-20 11:07:51 -04:00
Cory LaViska
802363e1da Merge branch 'christoshrousis-test/badge' into next 2021-09-20 11:06:09 -04:00
Cory LaViska
1054d5bb74 Merge branch 'test/badge' of https://github.com/christoshrousis/shoelace into christoshrousis-test/badge 2021-09-20 11:04:49 -04:00
Cory LaViska
f5d143710e fixes #528 2021-09-20 10:41:10 -04:00
Christos Hrousis
c025208bf5 test: first pass for badge test 2021-09-20 18:11:00 +10:00
Cory LaViska
7306e39f3e Add no-codepen for previews with assets 2021-09-19 12:01:04 -04:00
Cory LaViska
0d30d183df fix typo 2021-09-19 11:43:16 -04:00
Cory LaViska
cd504a4127 fix color 2021-09-19 11:42:33 -04:00
Cory LaViska
bba41402aa update progress ring to use CSS 2021-09-17 18:15:13 -04:00
Cory LaViska
e1f129abc5 improve spinner animation and API 2021-09-17 17:48:44 -04:00
Cory LaViska
7e6c6924f2 make search a bit fuzzier 2021-09-17 16:27:11 -04:00
Cory LaViska
185fc4c942 add file name to search index 2021-09-17 16:26:57 -04:00
Cory LaViska
5f7c6b307f improve search height 2021-09-17 16:13:29 -04:00
Cory LaViska
a9f80467c3 remove gap 2021-09-17 16:09:32 -04:00
Jake Patterson
89fb1a8804 fix broken link design token (#526) 2021-09-15 09:22:38 -04:00
Cory LaViska
54bfce0d5d add link 2021-09-13 08:12:51 -04:00
Cory LaViska
dca1d1b413 update docs 2021-09-13 07:54:05 -04:00
Cory LaViska
4b07ee40a7 fixes #523 2021-09-12 19:57:48 -04:00
Cory LaViska
d8644c940b add links to description 2021-09-11 09:44:47 -04:00
Cory LaViska
7df70abb48 no longer true 2021-09-09 10:41:23 -04:00
Cory LaViska
6e3c5c0388 reduce member boost 2021-09-09 10:17:55 -04:00
Cory LaViska
6cbe2e288b fix home button in search 2021-09-09 09:23:45 -04:00
Cory LaViska
1f95c6ca6e 2.0.0-beta.51 2021-09-09 09:14:51 -04:00
Cory LaViska
ac6ae43449 add metadata to search index 2021-09-09 09:13:55 -04:00
Cory LaViska
ba3117f435 fix comment 2021-09-09 09:13:29 -04:00
Cory LaViska
fd449f68d9 update changelog 2021-09-09 08:49:12 -04:00
Cory LaViska
574947656c add importing section to docs 2021-09-09 08:46:37 -04:00
Cory LaViska
5b1b704d1d more mobile tweaks 2021-09-09 07:46:04 -04:00
Cory LaViska
abbdb207e0 fix mobile styles 2021-09-09 07:43:17 -04:00
Cory LaViska
2d89fc945f remove elevation 2021-09-08 17:04:12 -04:00
Cory LaViska
25b130ce2c not too fuzzy 2021-09-08 12:25:02 -04:00
Cory LaViska
ca098eb171 fuzzy search and icon 2021-09-08 12:05:28 -04:00
Cory LaViska
f6ccd119e8 fix styles 2021-09-08 10:01:54 -04:00
Cory LaViska
7b1f99b41a update t-shirt tokens 2021-09-08 09:51:31 -04:00
Cory LaViska
4193ee1980 use focus-visible for code block buttons 2021-09-08 07:37:22 -04:00
Cory LaViska
c718012c3e remove unused code 2021-09-08 07:37:07 -04:00
Cory LaViska
aca5c8af73 updates 2021-09-08 07:25:07 -04:00
Cory LaViska
2d4a699790 no backslashes 2021-09-08 07:18:23 -04:00
Cory LaViska
fc0475b2c5 adjust for mobile 2021-09-08 07:16:05 -04:00
Cory LaViska
0f6e13b8c9 update changelog 2021-09-07 16:32:21 -04:00
Cory LaViska
c60cfae677 search styles 2021-09-07 15:05:57 -04:00
Cory LaViska
ded05cf079 improve search 2021-09-07 14:44:46 -04:00
Cory LaViska
c360d471cd fix ios 2021-09-07 13:21:22 -04:00
Cory LaViska
382a39e6ed fix build loop 2021-09-07 13:13:18 -04:00
Cory LaViska
bfa1499889 less noise 2021-09-07 13:13:02 -04:00
Cory LaViska
053a5e9bd7 generate at build time 2021-09-07 11:53:32 -04:00
Cory LaViska
3677f6cf0f add dep 2021-09-07 11:53:19 -04:00
Cory LaViska
03fb75f030 custom search 2021-09-07 11:52:58 -04:00
Cory LaViska
8e209d2767 update docs 2021-09-07 08:03:59 -04:00
Cory LaViska
0cf77a4115 update docs 2021-09-06 19:45:48 -04:00
Cory LaViska
14368c4ce2 Fix comment 2021-09-06 19:22:02 -04:00
Cory LaViska
e74838fdc1 adjust default button elevation 2021-09-03 10:06:09 -04:00
Cory LaViska
3f64b13d70 add missing tokens 2021-09-03 08:35:29 -04:00
Cory LaViska
613f4b6440 remove icon dep; improve copy animation 2021-09-03 08:21:16 -04:00
Cory LaViska
1bfa4c6ba8 reduce size 2021-09-03 08:09:07 -04:00
Cory LaViska
9089473cf0 improve elevation + overlay tokens 2021-09-03 08:03:32 -04:00
Cory LaViska
687eb7d0dc fix buttons on mobile 2021-09-02 10:57:59 -04:00
Shanyu Cui
17df0e3cd3 Merge branch 'next' of github.com:shoelace-style/shoelace into next 2021-07-16 02:04:07 -07:00
Shanyu Cui
76df2fd204 update hasSlot selector to search top-level only 2021-07-16 02:03:02 -07:00
162 changed files with 5692 additions and 1077 deletions

View File

@@ -1,38 +1,36 @@
---
name: Bug Report
about: Create a report to help us improve.
about: Create a bug report to help us fix a demonstrable problem with code in the library.
title: ''
labels: bug
assignees: claviska
---
**Describe the bug**
A clear and concise description of what the bug is.
### Describe the bug
A bug is _a demonstrable problem_ caused by code in the library. Please provide a clear and concise description of what the bug is here.
**To Reproduce**
### To Reproduce
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
2. Click on '...'
3. Scroll down to '...'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
### Demo
**Screenshots**
If applicable, add screenshots to help explain your problem.
If the bug isn't obvious, please provide a link to a CodePen or Fiddle with a minimal reproduction. Bugs that have repros get attention faster than those that don't.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
Tip: use the CodePen button on any example in the docs!
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
### Screenshots
If applicable, add screenshots to help explain the bug.
**Additional context**
Add any other context about the problem here.
### Browser / OS
- OS: [e.g. Mac, Windows]
- Browser [e.g. Chrome, Firefox, Safari]
- Browser version [e.g. 22]
### Additional information
Provide any additional information about the bug here.

View File

@@ -7,14 +7,11 @@ assignees: claviska
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
### What issue are you having?
Provide a clear and concise description of the problem you're facing.
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
### Describe the solution you'd like
How would you like to see the library solve it?
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
### Describe alternatives you've considered
In what ways have you tried to solve this with the current version?

31
.github/workflows/node.js.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
name: Node.js CI
on:
push:
branches: [ next ]
pull_request:
branches: [ next ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 16.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run build --if-present
- run: npm test

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
.DS_Store
.cache
docs/dist
docs/search.json
dist
examples
node_modules

View File

@@ -23,7 +23,7 @@ Twitter: [@shoelace_style](https://twitter.com/shoelace_style)
## Shoemakers 🥾
Shoemakers, or "Shoelace developers," can use this documentation to learn how to build Shoelace from source. You will need Node >= 12.10.0 to build and run the project locally.
Shoemakers, or "Shoelace developers," can use this documentation to learn how to build Shoelace from source. You will need Node >= 14 to build and run the project locally. It is preferred, but not required, to use npm 7.
**You don't need to do any of this to use Shoelace!** This page is for people who want to contribute to the project, tinker with the source, or create a custom build of Shoelace.

View File

@@ -3,6 +3,7 @@ import commentParser from 'comment-parser';
const packageData = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
const { name, description, version, author, homepage, license } = packageData;
const noDash = string => string.replace(/^\s?-/, '').trim();
export default {
globs: ['src/components/**/*.ts'],
@@ -47,7 +48,7 @@ export default {
}
classDoc['animations'].push({
name: t.name,
description: t.description
description: noDash(t.description)
});
break;

View File

@@ -1,3 +1,5 @@
# Not Found
Sorry, I couldn't find that page.
<img class="not-found-image" src="/assets/images/undraw-not-found.svg" alt="Cute monsters hiding behind a tree">
Sorry, I couldn't find that page. Have you tried pressing <kbd>/</kbd> to search?

View File

@@ -21,8 +21,10 @@
- [Card](/components/card)
- [Checkbox](/components/checkbox)
- [Color Picker](/components/color-picker)
- [Context Menu](/components/context-menu)
- [Details](/components/details)
- [Dialog](/components/dialog)
- [Divider](/components/divider)
- [Drawer](/components/drawer)
- [Dropdown](/components/dropdown)
- [Form](/components/form)
@@ -31,7 +33,6 @@
- [Image Comparer](/components/image-comparer)
- [Input](/components/input)
- [Menu](/components/menu)
- [Menu Divider](/components/menu-divider)
- [Menu Item](/components/menu-item)
- [Menu Label](/components/menu-label)
- [Progress Bar](/components/progress-bar)
@@ -54,11 +55,13 @@
<!--plop:component-->
- Utilities
- [Animated Image](/components/animated-image)
- [Animation](/components/animation)
- [Format Bytes](/components/format-bytes)
- [Format Date](/components/format-date)
- [Format Number](/components/format-number)
- [Include](/components/include)
- [Mutation Observer](/components/mutation-observer)
- [Relative Time](/components/relative-time)
- [Resize Observer](/components/resize-observer)
- [Responsive Media](/components/responsive-media)
@@ -73,5 +76,6 @@
- [Z-index](/tokens/z-index)
- Tutorials
- [Integrating with Laravel](/tutorials/integrating-with-laravel)
- [Integrating with NextJS](/tutorials/integrating-with-nextjs)
- [Integrating with Rails](/tutorials/integrating-with-rails)

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 709 KiB

After

Width:  |  Height:  |  Size: 688 KiB

BIN
docs/assets/images/tie.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
docs/assets/images/walk.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -120,7 +120,7 @@
z-index: 1;
}
.code-block__button:focus {
.code-block__button:focus-visible {
outline: none;
color: rgb(var(--sl-color-primary-600));
border-color: rgb(var(--sl-color-primary-400));
@@ -193,7 +193,7 @@
display: none;
}
.markdown-section .docsify-copy-code-button:focus {
.markdown-section .docsify-copy-code-button:focus-visible {
box-shadow: 0 0 0 3px rgb(var(--sl-color-neutral-500) / 50%);
}

View File

@@ -28,6 +28,22 @@
hook.afterEach(function (html, next) {
const domParser = new DOMParser();
const doc = domParser.parseFromString(html, 'text/html');
const codePenButton = `
<button type="button" class="code-block__button code-block__button--codepen" title="Edit on CodePen">
<svg
width="138"
height="26"
viewBox="0 0 138 26"
fill="none"
stroke="currentColor"
stroke-width="2.3"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M80 6h-9v14h9 M114 6h-9 v14h9 M111 13h-6 M77 13h-6 M122 20V6l11 14V6 M22 16.7L33 24l11-7.3V9.3L33 2L22 9.3V16.7z M44 16.7L33 9.3l-11 7.4 M22 9.3l11 7.3 l11-7.3 M33 2v7.3 M33 16.7V24 M88 14h6c2.2 0 4-1.8 4-4s-1.8-4-4-4h-6v14 M15 8c-1.3-1.3-3-2-5-2c-4 0-7 3-7 7s3 7 7 7 c2 0 3.7-0.8 5-2 M64 13c0 4-3 7-7 7h-5V6h5C61 6 64 9 64 13z" />
</svg>
</button>
`;
[...doc.querySelectorAll('code[class^="lang-"]')].map(code => {
if (code.classList.contains('preview')) {
@@ -66,11 +82,7 @@
</svg>
</button>
<button type="button" class="code-block__button code-block__button--codepen" title="Edit on CodePen">
<svg width="138" height="26" viewBox="0 0 138 26" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round">
<path d="M80 6h-9v14h9 M114 6h-9 v14h9 M111 13h-6 M77 13h-6 M122 20V6l11 14V6 M22 16.7L33 24l11-7.3V9.3L33 2L22 9.3V16.7z M44 16.7L33 9.3l-11 7.4 M22 9.3l11 7.3 l11-7.3 M33 2v7.3 M33 16.7V24 M88 14h6c2.2 0 4-1.8 4-4s-1.8-4-4-4h-6v14 M15 8c-1.3-1.3-3-2-5-2c-4 0-7 3-7 7s3 7 7 7 c2 0 3.7-0.8 5-2 M64 13c0 4-3 7-7 7h-5V6h5C61 6 64 9 64 13z"/>
</svg>
</button>
${!code.classList.contains('no-codepen') ? codePenButton : ''}
</div>
</div>
`;
@@ -119,26 +131,10 @@
document.documentElement.removeEventListener('touchend', dragStop, false);
};
const handleKeyDown = event => {
if (['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) {
const currentWidth = preview.clientWidth;
const maxWidth = preview.parentElement.clientWidth;
const incr = event.shiftKey ? 100 : 10;
event.preventDefault();
if (event.key === 'ArrowLeft') setWidth(currentWidth - incr);
if (event.key === 'ArrowRight') setWidth(currentWidth + incr);
if (event.key === 'Home') setWidth(0);
if (event.key === 'End') setWidth(maxWidth);
}
};
const setWidth = width => (preview.style.width = width + 'px');
resizer.addEventListener('mousedown', dragStart);
resizer.addEventListener('touchstart', dragStart);
resizer.addEventListener('keydown', handleKeyDown);
}, false);
});
});

View File

@@ -11,7 +11,6 @@
<thead>
<tr>
<th>Name</th>
<th>Attribute</th>
<th>Description</th>
<th>Reflects</th>
<th>Type</th>
@@ -21,18 +20,40 @@
<tbody>
${props
.map(prop => {
const hasAttribute = !!prop.attribute;
const isAttributeDifferent = prop.attribute !== prop.name;
let attributeInfo = '';
if (!hasAttribute) {
attributeInfo = `<br><small>(property only)</small>`;
} else if (isAttributeDifferent) {
attributeInfo = `
<br>
<sl-tooltip content="This attribute is different than the property">
<small>
<code class="nowrap">
${escapeHtml(prop.attribute)}
</code>
</small>
</sl-tooltip>`;
}
return `
<tr>
<td class="nowrap"><code>${escapeHtml(prop.name)}</code></td>
<td class="nowrap">${prop.attribute ? `<code>${escapeHtml(prop.attribute)}</code>` : '-'}</td>
<td>${escapeHtml(prop.description)}</td>
<td>
<code class="nowrap">${escapeHtml(prop.name)}</code>
${attributeInfo}
</td>
<td>
${escapeHtml(prop.description)}
</td>
<td style="text-align: center;">${
prop.reflects ? '<sl-icon label="yes" name="check"></sl-icon>' : ''
}</td>
<td>${prop.type?.text ? `<code>${escapeHtml(prop.type?.text || '')}</code>` : '-'}</td>
<td>${prop.default ? `<code>${escapeHtml(prop.default)}</code>` : '-'}</td>
</tr>
`;
`;
})
.join('')}
</tbody>
@@ -55,12 +76,12 @@
${events
.map(
event => `
<tr>
<td><code class="nowrap">${escapeHtml(event.name)}</code></td>
<td>${escapeHtml(event.description)}</td>
<td>${event.type?.text ? `<code>${escapeHtml(event.type?.text)}` : '-'}</td>
</tr>
`
<tr>
<td><code class="nowrap">${escapeHtml(event.name)}</code></td>
<td>${escapeHtml(event.description)}</td>
<td>${event.type?.text ? `<code>${escapeHtml(event.type?.text)}` : '-'}</td>
</tr>
`
)
.join('')}
</tbody>
@@ -146,11 +167,11 @@
${styles
.map(
style => `
<tr>
<td><code>${escapeHtml(style.name)}</code></td>
<td>${escapeHtml(style.description)}</td>
</tr>
`
<tr>
<td><code>${escapeHtml(style.name)}</code></td>
<td>${escapeHtml(style.description)}</td>
</tr>
`
)
.join('')}
</tbody>
@@ -172,11 +193,11 @@
${parts
.map(
part => `
<tr>
<td class="nowrap"><code>${escapeHtml(part.name)}</code></td>
<td>${escapeHtml(part.description)}</td>
</tr>
`
<tr>
<td class="nowrap"><code>${escapeHtml(part.name)}</code></td>
<td>${escapeHtml(part.description)}</td>
</tr>
`
)
.join('')}
</tbody>
@@ -198,11 +219,11 @@
${animations
.map(
animation => `
<tr>
<td class="nowrap"><code>${escapeHtml(animation.name)}</code></td>
<td>${escapeHtml(animation.description)}</td>
</tr>
`
<tr>
<td class="nowrap"><code>${escapeHtml(animation.name)}</code></td>
<td>${escapeHtml(animation.description)}</td>
</tr>
`
)
.join('')}
</tbody>
@@ -255,6 +276,9 @@
metadata.modules?.map(module => {
module.declarations?.map(declaration => {
if (declaration.customElement) {
// Generate the dist path based on the src path and attach it to the component
declaration.path = module.path.replace(/^src\//, 'dist/').replace(/\.ts$/, '.js');
allComponents.push(declaration);
}
});
@@ -429,6 +453,41 @@
`;
}
if (component.path) {
/* prettier-ignore */
result += `
## Importing
<sl-tab-group>
<sl-tab slot="nav" panel="cdn" active>CDN</sl-tab>
<sl-tab slot="nav" panel="bundler">Bundler</sl-tab>
<sl-tab slot="nav" panel="react">React</sl-tab>
<sl-tab-panel name="cdn">\n
To import this component from [the CDN](https://www.jsdelivr.com/package/npm/@shoelace-style/shoelace):
\`\`\`js
import 'https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${metadata.package.version}/${component.path}';
\`\`\`
</sl-tab-panel>
<sl-tab-panel name="bundler">\n
To import this component using [a bundler](/getting-started/installation#bundling):
\`\`\`js
import '@shoelace-style/shoelace/${component.path}';
\`\`\`
</sl-tab-panel>
<sl-tab-panel name="react">\n
To import this component using [\`@shoelace-style/react\`](https://www.npmjs.com/package/@shoelace-style/react):
\`\`\`js
import '@shoelace-style/react/dist/${component.tagName.replace(/^sl-/, '')}';
\`\`\`
</sl-tab-panel>
</sl-tab-group>
`;
}
// Strip whitespace so markdown doesn't process things as code blocks
return result.replace(/^ +| +$/gm, '');
});

1310
docs/assets/plugins/search/lunr.min.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,172 @@
body.site-search-visible {
overflow: hidden;
}
.sidebar .search-box {
margin: 1.25rem 26px;
}
.sidebar .search-box kbd {
margin-top: 2px;
}
/* Site search */
.site-search {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
}
.site-search[hidden] {
display: none;
}
.site-search__overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgb(var(--sl-overlay-background-color) / var(--sl-overlay-opacity));
z-index: -1;
}
.site-search__panel {
display: flex;
flex-direction: column;
max-width: 460px;
max-height: calc(100vh - 20rem);
background-color: rgb(var(--sl-surface-base-alt));
border-radius: var(--sl-border-radius-large);
box-shadow: var(--sl-shadow-x-large);
margin: 10rem auto;
}
@media screen and (max-width: 900px) {
.site-search__panel {
max-width: 100%;
max-height: calc(92vh - 120px); /* allow iOS browser chrome */
margin: 4vh var(--sl-spacing-medium);
}
}
.site-search__input::part(base) {
border: none;
background: transparent;
border-radius: var(--sl-border-radius-large);
}
.site-search__input:focus-within::part(base) {
outline: none;
box-shadow: none;
}
.site-search__input {
--sl-input-height-large: 4rem;
}
.site-search__body {
flex: 1 1 auto;
overflow: auto;
}
.site-search--has-results .site-search__body {
border-top: solid 1px rgb(var(--sl-color-neutral-200));
}
.site-search__results {
display: none;
line-height: var(--sl-line-height-dense);
list-style: none;
padding: var(--sl-spacing-x-small) 0;
margin: 0;
}
.site-search--has-results .site-search__results {
display: block;
}
.site-search__results a {
display: block;
text-decoration: none;
padding: var(--sl-spacing-x-small) var(--sl-spacing-large);
}
.site-search__results li a:hover,
.site-search__results li a:hover small {
background-color: rgb(var(--sl-color-neutral-100));
}
.site-search__results li[aria-selected='true'] a,
.site-search__results li[aria-selected='true'] a small,
.site-search__results li[aria-selected='true'] a sl-icon {
outline: none;
color: rgb(var(--sl-color-neutral-0));
background-color: rgb(var(--sl-color-primary-600));
}
.site-search__results h3 {
font-weight: var(--sl-font-weight-semibold);
margin: 0;
}
.site-search__results small {
display: block;
color: rgb(var(--sl-color-neutral-600));
}
.site-search__result {
padding: 0;
margin: 0;
}
.site-search__result a {
display: flex;
align-items: center;
gap: var(--sl-spacing-medium);
}
.site-search__result-icon {
flex: 0 0 auto;
display: flex;
color: rgb(var(--sl-color-neutral-400));
font-size: var(--sl-font-size-x-large);
}
.site-search__result-description {
flex: 1 1 auto;
}
.site-search__empty {
display: none;
border-top: solid 1px rgb(var(--sl-color-neutral-200));
text-align: center;
padding: var(--sl-spacing-x-large);
}
.site-search--no-results .site-search__empty {
display: block;
}
.site-search__footer {
display: flex;
justify-content: center;
gap: var(--sl-spacing-large);
border-top: solid 1px rgb(var(--sl-color-neutral-200));
border-bottom-left-radius: inherit;
border-bottom-right-radius: inherit;
padding: var(--sl-spacing-medium);
}
.site-search__footer small {
color: rgb(var(--sl-color-neutral-700));
}
@media screen and (max-width: 900px) {
.site-search__footer {
display: none;
}
}

View File

@@ -0,0 +1,293 @@
(() => {
if (!window.$docsify) {
throw new Error('Docsify must be loaded before installing this plugin.');
}
window.$docsify.plugins.push((hook, vm) => {
// Append the search box to the sidebar
hook.mounted(function () {
const appName = document.querySelector('.sidebar .app-name');
const searchBox = document.createElement('div');
searchBox.classList.add('search-box');
searchBox.innerHTML = `
<sl-input
type="search"
placeholder="Search"
clearable
pill
>
<sl-icon slot="prefix" name="search"></sl-icon>
<kbd slot="suffix" title="Press / to search">/</kbd>
</sl-input>
`;
const searchBoxInput = searchBox.querySelector('sl-input');
appName.insertAdjacentElement('afterend', searchBox);
// Show the search panel when the search is clicked
searchBoxInput.addEventListener('mousedown', event => {
event.preventDefault();
show();
});
// Show the search panel when a key is pressed
searchBoxInput.addEventListener('keydown', event => {
if (event.key === 'Tab') {
return;
}
// Pass the character that was typed through to the search input
if (event.key.length === 1) {
event.preventDefault();
input.value = event.key;
show();
}
});
});
// Append the search panel to the body
const siteSearch = document.createElement('div');
siteSearch.classList.add('site-search');
siteSearch.hidden = true;
siteSearch.innerHTML = `
<div class="site-search__overlay"></div>
<div class="site-search__panel">
<header class="site-search__header">
<sl-input
class="site-search__input"
type="search"
placeholder="Search this site"
size="large"
clearable
>
<sl-icon slot="prefix" name="search"></sl-icon>
</sl-input>
</header>
<div class="site-search__body">
<ul class="site-search__results"></ul>
<div class="site-search__empty">No results found.</div>
</div>
<footer class="site-search__footer">
<small><kbd>↑</kbd> <kbd>↓</kbd> navigate</small>
<small><kbd>↲</kbd> select</small>
<small><kbd>esc</kbd> close</small>
</footer>
</div>
`;
document.body.append(siteSearch);
const searchButtons = [...document.querySelectorAll('[data-site-search]')];
const overlay = siteSearch.querySelector('.site-search__overlay');
const panel = siteSearch.querySelector('.site-search__panel');
const input = siteSearch.querySelector('.site-search__input');
const results = siteSearch.querySelector('.site-search__results');
const animationDuration = 150;
const searchDebounce = 200;
let isShowing = false;
let searchTimeout;
let searchIndex;
let map;
// Load search data
const searchData = fetch('../../../search.json')
.then(res => res.json())
.then(data => {
searchIndex = lunr.Index.load(data.searchIndex);
map = data.map;
});
async function show() {
isShowing = true;
document.body.classList.add('site-search-visible');
siteSearch.hidden = false;
input.focus();
updateResults();
await Promise.all([
panel.animate(
[
{ opacity: 0, transform: 'scale(.9)' },
{ opacity: 1, transform: 'scale(1)' }
],
{ duration: animationDuration }
).finished,
overlay.animate([{ opacity: 0 }, { opacity: 1 }], { duration: animationDuration }).finished
]);
document.addEventListener('mousedown', handleDocumentMouseDown);
document.addEventListener('keydown', handleDocumentKeyDown);
document.addEventListener('focusin', handleDocumentFocusIn);
}
async function hide() {
isShowing = false;
document.body.classList.remove('site-search-visible');
await Promise.all([
panel.animate(
[
{ opacity: 1, transform: 'scale(1)' },
{ opacity: 0, transform: 'scale(.9)' }
],
{ duration: animationDuration }
).finished,
overlay.animate([{ opacity: 1 }, { opacity: 0 }], { duration: animationDuration }).finished
]);
siteSearch.hidden = true;
input.value = '';
updateResults();
document.removeEventListener('mousedown', handleDocumentMouseDown);
document.removeEventListener('keydown', handleDocumentKeyDown);
document.removeEventListener('focusin', handleDocumentFocusIn);
}
function handleInput() {
// Debounce search queries
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => updateResults(input.value), searchDebounce);
}
function handleDocumentFocusIn(event) {
// Close when focus leaves the panel
if (event.target.closest('.site-search__panel') !== panel) {
hide();
}
}
function handleDocumentMouseDown(event) {
// Close when clicking outside of the panel
if (event.target.closest('.site-search__overlay') === overlay) {
hide();
}
}
function handleDocumentKeyDown(event) {
// Close when pressing escape
if (event.key === 'Escape') {
event.preventDefault();
hide();
return;
}
// Handle keyboard selections
if (['ArrowDown', 'ArrowUp', 'Home', 'End', 'Enter'].includes(event.key)) {
event.preventDefault();
const currentEl = results.querySelector('[aria-selected="true"]');
const items = [...results.querySelectorAll('li')];
const index = items.indexOf(currentEl);
let nextEl;
if (items.length === 0) {
return;
}
switch (event.key) {
case 'ArrowUp':
nextEl = items[Math.max(0, index - 1)];
break;
case 'ArrowDown':
nextEl = items[Math.min(items.length - 1, index + 1)];
break;
case 'Home':
nextEl = items[0];
break;
case 'End':
nextEl = items[items.length - 1];
break;
case 'Enter':
currentEl?.querySelector('a')?.click();
break;
}
// Update the selected item
items.map(item => {
if (item === nextEl) {
item.setAttribute('aria-selected', 'true');
nextEl.scrollIntoView({ block: 'nearest' });
} else {
item.setAttribute('aria-selected', 'false');
}
});
return;
}
}
async function updateResults(query = '') {
try {
await searchIndex;
const hasQuery = query.length > 0;
let matches = hasQuery ? searchIndex.search(`${query}`) : [];
// Fall back to a fuzzy search if no matches are found
if (matches.length === 0 && hasQuery) {
matches = searchIndex.search(`${query}~2`);
}
let hasResults = hasQuery && matches.length > 0;
siteSearch.classList.toggle('site-search--has-results', hasQuery && hasResults);
siteSearch.classList.toggle('site-search--no-results', hasQuery && !hasResults);
panel.setAttribute('aria-expanded', hasQuery && hasResults ? 'true' : 'false');
results.innerHTML = '';
matches.map((match, index) => {
const page = map[match.ref];
const li = document.createElement('li');
const a = document.createElement('a');
let icon = 'file-text';
if (page.url.includes('getting-started/')) icon = 'lightbulb';
if (page.url.includes('resources/')) icon = 'book';
if (page.url.includes('components/')) icon = 'puzzle';
if (page.url.includes('tokens/')) icon = 'palette2';
if (page.url.includes('utilities/')) icon = 'wrench';
if (page.url.includes('tutorials/')) icon = 'joystick';
a.href = $docsify.routerMode === 'hash' ? `/#/${page.url}` : `/${page.url}`;
a.innerHTML = `
<div class="site-search__result-icon">
<sl-icon name="${icon}" aria-hidden="true"></sl-icon>
</div>
<div class="site-search__result__details">
<h3>${page.title}</h3>
<small>${page.url}</small>
</div>
`;
li.classList.add('site-search__result');
li.setAttribute('aria-selected', index === 0 ? 'true' : 'false');
li.appendChild(a);
results.appendChild(li);
});
} catch {
// Ignore query errors as the user types
}
}
// Show the search panel slash is pressed outside of a form element
document.addEventListener('keydown', event => {
if (
!isShowing &&
event.key === '/' &&
!event.composedPath().some(el => ['input', 'textarea'].includes(el?.tagName?.toLowerCase()))
) {
event.preventDefault();
show();
}
});
input.addEventListener('sl-input', handleInput);
// Close when a result is selected
results.addEventListener('click', event => {
if (event.target.closest('a')) {
hide();
}
});
});
})();

View File

@@ -1,14 +0,0 @@
(() => {
if (!window.$docsify) {
throw new Error('Docsify must be loaded before installing this plugin.');
}
window.$docsify.plugins.push((hook, vm) => {
hook.mounted(function () {
// Move search below the app name
const appName = document.querySelector('.sidebar .app-name');
const search = document.querySelector('.sidebar .search');
appName.insertAdjacentElement('afterend', search);
});
});
})();

View File

@@ -9,9 +9,21 @@
display: none;
}
.theme-picker sl-menu-label {
white-space: nowrap;
}
.theme-picker sl-menu-label kbd {
margin-left: 0.5rem;
}
@media screen and (max-width: 768px) {
.theme-picker {
top: 0.5rem;
right: 0.5rem;
}
.theme-picker sl-menu-label {
display: none;
}
}

View File

@@ -47,9 +47,10 @@
<sl-icon name="sun" label="Select Theme"></sl-icon>
</sl-button>
<sl-menu>
<sl-menu-label>Toggle <kbd>\\</kbd></sl-menu-label>
<sl-menu-item value="light">Light</sl-menu-item>
<sl-menu-item value="dark">Dark</sl-menu-item>
<sl-menu-divider></sl-menu-divider>
<sl-divider></sl-divider>
<sl-menu-item value="auto">Auto</sl-menu-item>
</sl-menu>
`;
@@ -63,6 +64,19 @@
// Update the theme when the preference changes
window.matchMedia('(prefers-color-scheme: dark)').addListener(event => setTheme(theme));
// Toggle themes when pressing backslash
document.addEventListener('keydown', event => {
if (
event.key === '\\' &&
!event.composedPath().some(el => ['input', 'textarea'].includes(el?.tagName?.toLowerCase()))
) {
event.preventDefault();
setTheme(isDark() ? 'light' : 'dark');
show();
}
});
// Set the intial theme and sync the UI
setTheme(theme);
});

View File

@@ -13,7 +13,7 @@ body {
font-size: var(--sl-font-size-medium);
font-weight: var(--sl-font-weight-normal);
letter-spacing: var(--sl-letter-spacing-normal);
background-color: rgb(var(--sl-color-neutral-0));
background-color: rgb(var(--sl-surface-base));
color: rgb(var(--sl-color-neutral-800));
line-height: var(--sl-line-height-normal);
}
@@ -32,13 +32,12 @@ strong {
/* Sidebar */
.sidebar {
background: rgb(var(--sl-color-neutral-0));
background-color: rgb(var(--sl-surface-base));
border-right: solid 1px rgb(var(--sl-color-neutral-200));
}
.sidebar .app-name {
padding: 0 1.5rem;
margin-top: 1.5rem;
}
.sidebar-version {
@@ -55,94 +54,6 @@ strong {
margin-top: 0;
}
/* Search */
.sidebar .search {
position: relative;
border: none;
}
.sidebar .search input[type='search'] {
background-color: rgb(var(--sl-input-background-color));
border: solid 1px rgb(var(--sl-input-border-color));
border-radius: var(--sl-border-radius-pill);
color: rgb(var(--sl-input-color));
padding-left: 1rem;
padding-right: 2rem;
margin: 0 1.25rem;
transition: var(--sl-transition-fast) color, var(--sl-transition-fast) border, var(--sl-transition-fast) box-shadow;
}
.sidebar .search input[type='search']::placeholder {
color: rgb(var(--sl-input-placeholder-color));
}
.sidebar .search input[type='search']:hover {
background-color: rgb(var(--sl-input-background-color-hover));
border-color: rgb(var(--sl-input-border-color-hover));
color: rgb(var(--sl-input-color-hover));
}
.sidebar .search input[type='search']:focus {
background-color: rgb(var(--sl-input-background-color-focus));
box-shadow: var(--sl-focus-ring);
border-color: rgb(var(--sl-input-border-color-focus));
color: rgb(var(--sl-input-color-focus));
outline: none;
}
.sidebar .input-wrap {
position: relative;
width: 100%;
padding: 0 0.25rem;
}
.sidebar .clear-button {
position: absolute;
right: 34px;
top: 7px;
width: 22px !important;
height: 22px !important;
}
.sidebar .clear-button svg {
transform: scale(0.75) !important;
}
.sidebar .clear-button svg circle {
fill: rgb(var(--sl-color-neutral-500));
}
.sidebar .clear-button svg path {
stroke: rgb(var(--sl-color-neutral-0));
}
.sidebar .clear-button:focus {
outline: none;
}
.search .results-panel {
margin-top: 1rem;
}
.search .matching-post {
border-bottom: solid 1px rgb(var(--sl-color-neutral-600)) !important;
padding: 0.25rem 1.5rem;
}
.search .matching-post a {
display: block;
border-radius: inherit;
padding: 0.5rem;
}
.search .matching-post h2 {
margin-bottom: 0;
}
.search .matching-post p {
margin-top: 0;
}
/* Sidebar toggle */
.sidebar-toggle {
top: 0.25rem;
@@ -150,7 +61,7 @@ strong {
width: 2rem;
height: 2rem;
border-radius: var(--sl-border-radius-medium);
background-color: rgb(var(--sl-color-neutral-0));
background-color: rgb(var(--sl-surface-base));
padding: 0.5rem;
}
@@ -249,6 +160,11 @@ strong {
max-width: 22rem;
}
.markdown-section .splash-start h1:first-of-type {
font-size: var(--sl-font-size-large);
margin: 0 0 0.5rem 0;
}
@media screen and (max-width: 1040px) {
.splash {
display: block;
@@ -334,7 +250,7 @@ strong {
}
.markdown-section h1 {
font-size: var(--sl-font-size-xx-large);
font-size: var(--sl-font-size-2x-large);
}
.markdown-section h2 {
@@ -375,12 +291,19 @@ strong {
padding: 2px 4px;
}
.markdown-section tr:nth-child(2n) code {
background-color: rgb(var(--sl-color-neutral-100));
}
kbd,
.markdown-section kbd {
font-family: var(--sl-font-mono);
font-size: 87.5%;
background-color: rgb(var(--sl-color-neutral-50));
border-radius: var(--sl-border-radius-small);
border: solid 1px rgb(var(--sl-color-neutral-200));
padding: 2px 4px;
box-shadow: inset 0 1px 0 rgb(var(--sl-color-neutral-0));
padding: 2px 5px;
}
/* Code blocks */
@@ -510,6 +433,11 @@ strong {
white-space: nowrap;
}
.markdown-section table sl-tooltip code {
border-bottom: dashed 1px rgb(var(--sl-color-neutral-300));
cursor: help;
}
/* Iframes */
.markdown-section iframe {
border: none;
@@ -604,11 +532,11 @@ strong {
}
.repo-button--twitter sl-icon {
color: rgb(var(--sl-color-sky-600));
color: rgb(var(--sl-color-sky-500));
}
@media screen and (max-width: 400px) {
.repo-button {
:not(.sidebar-buttons) > .repo-button {
width: 100%;
margin-bottom: 1rem;
}
@@ -662,13 +590,14 @@ body[data-page^='/tokens/'] .table-wrapper td:first-child code {
border-radius: 3px;
width: 4rem;
height: 4rem;
margin: 1rem;
}
/* Color palettes */
.color-palette {
display: grid;
grid-template-columns: 200px repeat(11, 1fr);
gap: 1rem var(--sl-spacing-xx-small);
gap: 1rem var(--sl-spacing-2x-small);
margin: 2rem 0;
}
@@ -706,3 +635,9 @@ body[data-page^='/tokens/'] .table-wrapper td:first-child code {
grid-column-start: span 6;
}
}
.not-found-image {
display: block;
max-width: 460px;
margin: 2rem 0;
}

View File

@@ -0,0 +1,63 @@
# Animated Image
[component-header:sl-animated-image]
A component for displaying animated GIFs and WEBPs that play and pause on interaction.
```html preview
<sl-animated-image
src="/assets/images/walk.gif"
alt="Animation of untied shoes walking on pavement"
></sl-animated-image>
```
## Examples
### WEBP Images
Both GIF and WEBP images are supported.
```html preview
<sl-animated-image
src="/assets/images/tie.webp"
alt="Animation of a shoe being tied"
></sl-animated-image>
```
### Setting a Width and Height
To set a custom size, apply a width and/or height to the host element.
```html preview
<sl-animated-image
src="/assets/images/walk.gif"
alt="Animation of untied shoes walking on pavement"
style="width: 150px; height: 200px;"
>
</sl-animated-image>
```
### Customizing the Control Box
You can change the appearance and location of the control box by targeting the `control-box` part in your styles.
```html preview
<sl-animated-image
src="/assets/images/walk.gif"
alt="Animation of untied shoes walking on pavement"
class="animated-image-custom-control-box"
></sl-animated-image>
<style>
.animated-image-custom-control-box::part(control-box) {
top: auto;
right: auto;
bottom: 1rem;
left: 1rem;
background-color: deeppink;
color: white;
}
</style>
```
[component-metadata:sl-animated-image]

View File

@@ -169,7 +169,7 @@ Supply your own [keyframe formats](https://developer.mozilla.org/en-US/docs/Web/
.animation-keyframes .box {
width: 100px;
height: 100px;
background-color: rgb(var(--sl-color-primary-500));
background-color: rgb(var(--sl-color-primary-600));
}
</style>
```

View File

@@ -2,7 +2,7 @@
[component-header:sl-breadcrumb-item]
Breadcrumb Items are used inside breadcrumbs to represent different links.
Breadcrumb Items are used inside [breadcrumbs](/components/breadcrumb) to represent different links.
```html preview
<sl-breadcrumb>

View File

@@ -49,7 +49,7 @@ Use the `separator` slot to change the separator that goes between breadcrumb it
```html preview
<sl-breadcrumb>
<sl-icon name="dot" slot="separator" id="dotty"></sl-icon>
<sl-icon name="dot" slot="separator"></sl-icon>
<sl-breadcrumb-item>First</sl-breadcrumb-item>
<sl-breadcrumb-item>Second</sl-breadcrumb-item>
<sl-breadcrumb-item>Third</sl-breadcrumb-item>

View File

@@ -116,7 +116,7 @@ Pill buttons are supported through the button's `pill` attribute.
### Dropdowns in Button Groups
Dropdowns can be placed inside button groups as long as the trigger is a `<sl-button>` element.
Dropdowns can be placed inside button groups as long as the trigger is an `<sl-button>` element.
```html preview
<sl-button-group>

View File

@@ -33,6 +33,19 @@ Use the `size` attribute to change a button's size.
<sl-button size="large">Large</sl-button>
```
### Outline Buttons
Use the `outline` attribute to draw outlined buttons with transparent backgrounds.
```html preview
<sl-button type="default" outline>Default</sl-button>
<sl-button type="primary" outline>Primary</sl-button>
<sl-button type="success" outline>Success</sl-button>
<sl-button type="neutral" outline>Neutral</sl-button>
<sl-button type="warning" outline>Warning</sl-button>
<sl-button type="danger" outline>Danger</sl-button>
```
### Pill Buttons
Use the `pill` attribute to give buttons rounded edges.

View File

@@ -0,0 +1,140 @@
# Context Menu
[component-header:sl-context-menu]
Context menus offer additional options through a menu that opens at the pointer's location, usually activated by a right-click.
Context menus are designed to work with [menus](/components/menu) and [menu items](/components/menu-item). The menu must include `slot="menu"`. Other content you provide will be part of the context menu's target area.
```html preview
<sl-context-menu>
<div style="height: 200px; background: rgb(var(--sl-color-neutral-100)); display: flex; align-items: center; justify-content: center; padding: 1rem;">
Right-click to activate the context menu
</div>
<sl-menu slot="menu">
<sl-menu-item value="undo">Undo</sl-menu-item>
<sl-menu-item value="redo">Redo</sl-menu-item>
<sl-divider></sl-divider>
<sl-menu-item value="cut">Cut</sl-menu-item>
<sl-menu-item value="copy">Copy</sl-menu-item>
<sl-menu-item value="paste">Paste</sl-menu-item>
<sl-menu-item value="delete">Delete</sl-menu-item>
</sl-menu>
</sl-context-menu>
```
## Examples
### Handling Selections
The [menu component](/components/menu) emits an `sl-select` event when a menu item is selected. You can use this to handle selections. The selected item will be available in `event.detail.item`.
```html preview
<div class="context-menu-selections">
<sl-context-menu>
<div style="height: 200px; background: rgb(var(--sl-color-neutral-100)); display: flex; align-items: center; justify-content: center; padding: 1rem;">
Right-click to activate the context menu
</div>
<sl-menu slot="menu">
<sl-menu-item value="cut">Cut</sl-menu-item>
<sl-menu-item value="copy">Copy</sl-menu-item>
<sl-menu-item value="paste">Paste</sl-menu-item>
</sl-menu>
</sl-context-menu>
</div>
<script>
const container = document.querySelector('.context-menu-selections');
const menu = container.querySelector('sl-menu');
const result = container.querySelector('.result');
menu.addEventListener('sl-select', event => {
console.log(`You selected: ${event.detail.item.value}`);
});
</script>
```
### Inline
The context menu uses `display: contents`, so it will assume the shape of the content you slot in.
```html preview
<sl-context-menu>
<span style="background: rgb(var(--sl-color-neutral-100)); padding: .5rem 1rem;">
Right-click here
</span>
<sl-menu slot="menu">
<sl-menu-item value="cut">Cut</sl-menu-item>
<sl-menu-item value="copy">Copy</sl-menu-item>
<sl-menu-item value="paste">Paste</sl-menu-item>
</sl-menu>
</sl-context-menu>
```
### Placement
The preferred placement of the context menu can be set with the `placement` attribute. Note that the actual position may vary to ensure the menu remains in the viewport.
```html preview
<sl-context-menu placement="top-end">
<div style="height: 200px; background: rgb(var(--sl-color-neutral-100)); display: flex; align-items: center; justify-content: center; padding: 1rem;">
Right-click to activate the context menu
</div>
<sl-menu slot="menu">
<sl-menu-item value="undo">Undo</sl-menu-item>
<sl-menu-item value="redo">Redo</sl-menu-item>
<sl-divider></sl-divider>
<sl-menu-item value="cut">Cut</sl-menu-item>
<sl-menu-item value="copy">Copy</sl-menu-item>
<sl-menu-item value="paste">Paste</sl-menu-item>
<sl-menu-item value="delete">Delete</sl-menu-item>
</sl-menu>
</sl-context-menu>
```
### Detecting the Target Item
A single context menu can wrap a number of items. To detect the item that activated the context menu...
TODO
```html preview
<div class="context-menu-detecting">
<sl-context-menu>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
<li>Item 4</li>
<li>Item 5</li>
</ul>
<sl-menu slot="menu">
<sl-menu-item value="cut">Cut</sl-menu-item>
<sl-menu-item value="copy">Copy</sl-menu-item>
<sl-menu-item value="paste">Paste</sl-menu-item>
</sl-menu>
</sl-context-menu>
</div>
<style>
.context-menu-detecting ul {
max-width: 300px;
list-style: none;
padding: 0;
margin: 0;
}
.context-menu-detecting li {
background: rgb(var(--sl-color-neutral-100));
padding: .5rem 1rem;
margin: 0 0 2px 0;
}
</style>
```
[component-metadata:sl-context-menu]

View File

@@ -57,7 +57,7 @@ Details are designed to function independently, but you can simulate a group or
<style>
.details-group-example sl-details:not(:last-of-type) {
margin-bottom: var(--sl-spacing-xx-small);
margin-bottom: var(--sl-spacing-2x-small);
}
</style>
```

View File

@@ -0,0 +1,71 @@
# Divider
[component-header:sl-divider]
Dividers are used to visually separate or group elements.
```html preview
<sl-divider></sl-divider>
```
## Examples
### Width
Use the `--width` custom property to change the width of the divider.
```html preview
<sl-divider style="--width: 4px;"></sl-divider>
```
### Color
Use the `--color` custom property to change the color of the divider.
```html preview
<sl-divider style="--color: tomato;"></sl-divider>
```
### Spacing
Use the `--spacing` custom property to change the amount of space between the divider and it's neighboring elements.
```html preview
<div style="text-align: center;">
Above
<sl-divider style="--spacing: 2rem;"></sl-divider>
Below
</div>
```
### Vertical
Add the `vertical` attribute to draw the divider in a vertical orientation. The divider will span the full height of its container. Vertical dividers work especially well inside of a flex container.
```html preview
<div style="display: flex; align-items: center; height: 2rem;">
First
<sl-divider vertical></sl-divider>
Middle
<sl-divider vertical></sl-divider>
Last
</div>
```
### Menu Dividers
Use dividers in [menus](/components/menu) to visually group menu items.
```html preview
<sl-menu style="max-width: 200px; border: solid 1px rgb(var(--sl-panel-border-color)); border-radius: var(--sl-border-radius-medium);">
<sl-menu-item value="1">Option 1</sl-menu-item>
<sl-menu-item value="2">Option 2</sl-menu-item>
<sl-menu-item value="3">Option 3</sl-menu-item>
<sl-divider></sl-divider>
<sl-menu-item value="4">Option 4</sl-menu-item>
<sl-menu-item value="5">Option 5</sl-menu-item>
<sl-menu-item value="6">Option 6</sl-menu-item>
</sl-menu>
```
[component-metadata:sl-divider]

View File

@@ -15,10 +15,10 @@ Dropdowns are designed to work well with [menus](/components/menu) to provide a
<sl-menu-item>Dropdown Item 1</sl-menu-item>
<sl-menu-item>Dropdown Item 2</sl-menu-item>
<sl-menu-item>Dropdown Item 3</sl-menu-item>
<sl-menu-divider></sl-menu-divider>
<sl-divider></sl-divider>
<sl-menu-item checked>Checked</sl-menu-item>
<sl-menu-item disabled>Disabled</sl-menu-item>
<sl-menu-divider></sl-menu-divider>
<sl-divider></sl-divider>
<sl-menu-item>
Prefix
<sl-icon slot="prefix" name="gift"></sl-icon>
@@ -33,6 +33,33 @@ Dropdowns are designed to work well with [menus](/components/menu) to provide a
## Examples
### Getting the Selected Item
When dropdowns are used with [menus](/components/menu), you can listen for the `sl-select` event to determine which menu item was selected. The menu item element will be exposed in `event.detail.item`. You can set `value` props to make it easier to identify commands.
```html preview
<div class="dropdown-selection">
<sl-dropdown>
<sl-button slot="trigger" caret>Edit</sl-button>
<sl-menu>
<sl-menu-item value="cut">Cut</sl-menu-item>
<sl-menu-item value="copy">Copy</sl-menu-item>
<sl-menu-item value="paste">Paste</sl-menu-item>
</sl-menu>
</sl-dropdown>
</div>
<script>
const container = document.querySelector('.dropdown-selection');
const dropdown = container.querySelector('sl-dropdown');
dropdown.addEventListener('sl-select', event => {
const selectedItem = event.detail.item;
console.log(selectedItem.value);
});
</script>
```
### Placement
The preferred placement of the dropdown can be set with the `placement` attribute. Note that the actual position may vary to ensure the panel remains in the viewport.
@@ -44,7 +71,7 @@ The preferred placement of the dropdown can be set with the `placement` attribut
<sl-menu-item>Cut</sl-menu-item>
<sl-menu-item>Copy</sl-menu-item>
<sl-menu-item>Paste</sl-menu-item>
<sl-menu-divider></sl-menu-divider>
<sl-divider></sl-divider>
<sl-menu-item>Find</sl-menu-item>
<sl-menu-item>Replace</sl-menu-item>
</sl-menu>
@@ -62,7 +89,7 @@ The distance from the panel to the trigger can be customized using the `distance
<sl-menu-item>Cut</sl-menu-item>
<sl-menu-item>Copy</sl-menu-item>
<sl-menu-item>Paste</sl-menu-item>
<sl-menu-divider></sl-menu-divider>
<sl-divider></sl-divider>
<sl-menu-item>Find</sl-menu-item>
<sl-menu-item>Replace</sl-menu-item>
</sl-menu>
@@ -80,7 +107,7 @@ The offset of the panel along the trigger can be customized using the `skidding`
<sl-menu-item>Cut</sl-menu-item>
<sl-menu-item>Copy</sl-menu-item>
<sl-menu-item>Paste</sl-menu-item>
<sl-menu-divider></sl-menu-divider>
<sl-divider></sl-divider>
<sl-menu-item>Find</sl-menu-item>
<sl-menu-item>Replace</sl-menu-item>
</sl-menu>
@@ -121,57 +148,4 @@ Dropdown panels will be clipped if they're inside a container that has `overflow
</style>
```
### Getting the Selected Item
When dropdowns are used with [menus](/components/menu), you can listen for the `sl-select` event to determine which menu item was selected. The menu item element will be exposed in `event.detail.item`. You can set `value` props to make it easier to identify commands.
```html preview
<div class="dropdown-selection">
<sl-dropdown>
<sl-button slot="trigger" caret>Edit</sl-button>
<sl-menu>
<sl-menu-item value="cut">Cut</sl-menu-item>
<sl-menu-item value="copy">Copy</sl-menu-item>
<sl-menu-item value="paste">Paste</sl-menu-item>
</sl-menu>
</sl-dropdown>
</div>
<script>
const container = document.querySelector('.dropdown-selection');
const dropdown = container.querySelector('sl-dropdown');
dropdown.addEventListener('sl-select', event => {
const selectedItem = event.detail.item;
console.log(selectedItem.value);
});
</script>
```
Alternatively, you can listen for the `click` event on individual menu items. Note that, using this approach, disabled menu items will still emit a `click` event.
```html preview
<div class="dropdown-selection-alt">
<sl-dropdown>
<sl-button slot="trigger" caret>Edit</sl-button>
<sl-menu>
<sl-menu-item value="cut">Cut</sl-menu-item>
<sl-menu-item value="copy">Copy</sl-menu-item>
<sl-menu-item value="paste">Paste</sl-menu-item>
</sl-menu>
</sl-dropdown>
</div>
<script>
const container = document.querySelector('.dropdown-selection-alt');
const cut = container.querySelector('sl-menu-item[value="cut"]');
const copy = container.querySelector('sl-menu-item[value="copy"]');
const paste = container.querySelector('sl-menu-item[value="paste"]');
cut.addEventListener('click', () => console.log('cut'));
copy.addEventListener('click', () => console.log('copy'));
paste.addEventListener('click', () => console.log('paste'));
</script>
```
[component-metadata:sl-dropdown]

View File

@@ -385,7 +385,7 @@ Icons in this library are licensed under the [Apache 2.0 License](https://github
### Remix Icon
This will register the [Remix Icon](https://remixicon.com/) library using the jsDelivr CDN. This library has two variations: line (default) and fill (`*-fill`). It also groups icons by categories, so the name must include the category and icon separated by a slash. A mutator function is required to set the SVG's `fill` to `currentColor`.
This will register the [Remix Icon](https://remixicon.com/) library using the jsDelivr CDN. This library groups icons by categories, so the name must include the category and icon separated by a slash, as well as the `-line` or `-fill` suffix as needed. A mutator function is required to set the SVG's `fill` to `currentColor`.
Icons in this library are licensed under the [Apache 2.0 License](https://github.com/Remix-Design/RemixIcon/blob/master/License).
@@ -395,21 +395,21 @@ Icons in this library are licensed under the [Apache 2.0 License](https://github
registerIconLibrary('remixicon', {
resolver: name => {
const match = name.match(/^(.*?)\/(.*?)(-(fill))?$/);
const match = name.match(/^(.*?)\/(.*?)?$/);
match[1] = match[1].charAt(0).toUpperCase() + match[1].slice(1);
return `https://cdn.jsdelivr.net/npm/remixicon@2.5.0/icons/${match[1]}/${match[2]}${match[3] || '-line'}.svg`;
return `https://cdn.jsdelivr.net/npm/remixicon@2.5.0/icons/${match[1]}/${match[2]}.svg`;
},
mutator: svg => svg.setAttribute('fill', 'currentColor')
});
</script>
<div style="font-size: 24px;">
<sl-icon library="remixicon" name="business/cloud"></sl-icon>
<sl-icon library="remixicon" name="design/brush"></sl-icon>
<sl-icon library="remixicon" name="business/pie-chart"></sl-icon>
<sl-icon library="remixicon" name="development/bug"></sl-icon>
<sl-icon library="remixicon" name="media/image"></sl-icon>
<sl-icon library="remixicon" name="system/alert"></sl-icon>
<sl-icon library="remixicon" name="business/cloud-line"></sl-icon>
<sl-icon library="remixicon" name="design/brush-line"></sl-icon>
<sl-icon library="remixicon" name="business/pie-chart-line"></sl-icon>
<sl-icon library="remixicon" name="development/bug-line"></sl-icon>
<sl-icon library="remixicon" name="media/image-line"></sl-icon>
<sl-icon library="remixicon" name="system/alert-line"></sl-icon>
<br>
<sl-icon library="remixicon" name="business/cloud-fill"></sl-icon>
<sl-icon library="remixicon" name="design/brush-fill"></sl-icon>

View File

@@ -8,7 +8,7 @@ Included files are asynchronously requested using `window.fetch()`. Requests are
The included content will be inserted into the `<sl-include>` element's default slot so it can be easily accessed and styled through the light DOM.
```html preview
```html preview no-codepen
<sl-include src="/assets/examples/include.html"></sl-include>
```

View File

@@ -42,6 +42,14 @@ Add the `toggle-password` attribute to add a toggle button that will show the pa
<sl-input type="password" placeholder="Password Toggle" size="large" toggle-password></sl-input>
```
### Filled Inputs
Add the `filled` attribute to draw a filled input.
```html preview
<sl-input placeholder="Type something" filled></sl-input>
```
### Pill
Use the `pill` attribute to give inputs rounded edges.

View File

@@ -1,22 +0,0 @@
# Menu Divider
[component-header:sl-menu-divider]
Menu dividers are used to visually group menu items.
```html preview
<sl-menu
style="max-width: 200px; border: solid 1px rgb(var(--sl-panel-border-color)); border-radius: var(--sl-border-radius-medium);"
>
<sl-menu-item value="1">Option 1</sl-menu-item>
<sl-menu-item value="2">Option 2</sl-menu-item>
<sl-menu-divider></sl-menu-divider>
<sl-menu-item value="3">Option 3</sl-menu-item>
<sl-menu-item value="4">Option 4</sl-menu-item>
<sl-menu-divider></sl-menu-divider>
<sl-menu-item value="5">Option 5</sl-menu-item>
<sl-menu-item value="6">Option 6</sl-menu-item>
</sl-menu>
```
[component-metadata:sl-menu-divider]

View File

@@ -5,16 +5,14 @@
Menu items provide options for the user to pick from in a menu.
```html preview
<sl-menu
style="max-width: 200px; border: solid 1px rgb(var(--sl-panel-border-color)); border-radius: var(--sl-border-radius-medium);"
>
<sl-menu style="max-width: 200px; border: solid 1px rgb(var(--sl-panel-border-color)); border-radius: var(--sl-border-radius-medium);">
<sl-menu-item>Option 1</sl-menu-item>
<sl-menu-item>Option 2</sl-menu-item>
<sl-menu-item>Option 3</sl-menu-item>
<sl-menu-divider></sl-menu-divider>
<sl-divider></sl-divider>
<sl-menu-item checked>Checked</sl-menu-item>
<sl-menu-item disabled>Disabled</sl-menu-item>
<sl-menu-divider></sl-menu-divider>
<sl-divider></sl-divider>
<sl-menu-item>
Prefix Icon
<sl-icon slot="prefix" name="gift"></sl-icon>
@@ -26,4 +24,82 @@ Menu items provide options for the user to pick from in a menu.
</sl-menu>
```
## Examples
### Checked
Use the `checked` attribute to draw menu items in a checked state.
```html preview
<sl-menu style="max-width: 200px; border: solid 1px rgb(var(--sl-panel-border-color)); border-radius: var(--sl-border-radius-medium);">
<sl-menu-item>Option 1</sl-menu-item>
<sl-menu-item checked>Option 2</sl-menu-item>
<sl-menu-item>Option 3</sl-menu-item>
</sl-menu>
```
### Disabled
Add the `disabled` attribute to disable the menu item so it cannot be selected.
```html preview
<sl-menu style="max-width: 200px; border: solid 1px rgb(var(--sl-panel-border-color)); border-radius: var(--sl-border-radius-medium);">
<sl-menu-item>Option 1</sl-menu-item>
<sl-menu-item disabled>Option 2</sl-menu-item>
<sl-menu-item>Option 3</sl-menu-item>
</sl-menu>
```
### Prefix & Suffix
Add content to the start and end of menu items using the `prefix` and `suffix` slots.
```html preview
<sl-menu style="max-width: 200px; border: solid 1px rgb(var(--sl-panel-border-color)); border-radius: var(--sl-border-radius-medium);">
<sl-menu-item>
<sl-icon slot="prefix" name="house"></sl-icon>
Home
</sl-menu-item>
<sl-menu-item>
<sl-icon slot="prefix" name="envelope"></sl-icon>
Messages
<sl-badge slot="suffix" type="primary" pill>12</sl-badge>
</sl-menu-item>
<sl-divider></sl-divider>
<sl-menu-item>
<sl-icon slot="prefix" name="gear"></sl-icon>
Settings
</sl-menu-item>
</sl-menu>
```
### Value & Selection
The `value` attribute can be used to assign a hidden value, such as a unique identifier, to a menu item. When an item is selected, the `sl-select` event will be emitted and a reference to the item will be available at `event.detail.item`. You can use this reference to access the selected item's value, its checked state, and more.
```html preview
<sl-menu class="menu-value" style="max-width: 200px; border: solid 1px rgb(var(--sl-panel-border-color)); border-radius: var(--sl-border-radius-medium);">
<sl-menu-item value="opt-1">Option 1</sl-menu-item>
<sl-menu-item value="opt-2">Option 2</sl-menu-item>
<sl-menu-item value="opt-3">Option 3</sl-menu-item>
</sl-menu>
<script>
const menu = document.querySelector('.menu-value');
menu.addEventListener('sl-select', event => {
const item = event.detail.item;
// Toggle checked state
item.checked = !item.checked;
// Log value
console.log(`Selected value: ${item.value}`);
});
</script>
```
[component-metadata:sl-menu-item]

View File

@@ -12,7 +12,7 @@ Menu labels are used to describe a group of menu items.
<sl-menu-item value="apple">Apple</sl-menu-item>
<sl-menu-item value="banana">Banana</sl-menu-item>
<sl-menu-item value="orange">Orange</sl-menu-item>
<sl-menu-divider></sl-menu-divider>
<sl-divider></sl-divider>
<sl-menu-label>Vegetables</sl-menu-label>
<sl-menu-item value="broccoli">Broccoli</sl-menu-item>
<sl-menu-item value="carrot">Carrot</sl-menu-item>

View File

@@ -4,13 +4,13 @@
Menus provide a list of options for the user to choose from.
You can use [menu items](/components/menu-item), [menu dividers](/components/menu-divider), and [menu labels](/components/menu-label) to compose a menu. Menus support keyboard interactions, including type-to-select an option.
You can use [menu items](/components/menu-item), [menu labels](/components/menu-label), and [dividers](/components/divider) to compose a menu. Menus support keyboard interactions, including type-to-select an option.
```html preview
<sl-menu style="max-width: 200px; border: solid 1px rgb(var(--sl-panel-border-color)); border-radius: var(--sl-border-radius-medium);">
<sl-menu-item value="undo">Undo</sl-menu-item>
<sl-menu-item value="redo">Redo</sl-menu-item>
<sl-menu-divider></sl-menu-divider>
<sl-divider></sl-divider>
<sl-menu-item value="cut">Cut</sl-menu-item>
<sl-menu-item value="copy">Copy</sl-menu-item>
<sl-menu-item value="paste">Paste</sl-menu-item>

View File

@@ -0,0 +1,104 @@
# Mutation Observer
[component-header:sl-mutation-observer]
The Mutation Observer component offers a thin, declarative interface to the [`MutationObserver API`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver).
The mutation observer will report changes to the content it wraps through the `sl-mutation` event. When emitted, a collection of [MutationRecord](https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord) objects will be attached to `event.detail` that contains information about how it changed.
```html preview
<div class="mutation-overview">
<sl-mutation-observer attr>
<sl-button type="primary">Click to mutate</sl-button>
</sl-mutation-observer>
<br>
👆 Click the button and watch the console
<script>
const container = document.querySelector('.mutation-overview');
const mutationObserver = container.querySelector('sl-mutation-observer');
const button = container.querySelector('sl-button');
const types = ['primary', 'success', 'neutral', 'warning', 'danger'];
let clicks = 0;
// Change the button's type attribute
button.addEventListener('click', () => {
clicks++;
button.setAttribute('type', types[clicks % types.length]);
});
// Log mutations
mutationObserver.addEventListener('sl-mutation', event => {
console.log(event.detail);
});
</script>
<style>
.mutation-overview sl-button {
margin-bottom: 1rem;
}
</style>
</div>
```
?> When you create a mutation observer, you must indicate what changes it should respond to by including at least one of `attr`, `child-list`, or `char-data`. If you don't specify at least one of these attributes, no mutation events will be emitted.
## Examples
### Child List
Use the `child-list` attribute to watch for new child elements that are added or removed.
```html preview
<div class="mutation-child-list">
<sl-mutation-observer child-list>
<div class="buttons">
<sl-button type="primary">Add button</sl-button>
</div>
</sl-mutation-observer>
👆 Add and remove buttons and watch the console
<script>
const container = document.querySelector('.mutation-child-list');
const mutationObserver = container.querySelector('sl-mutation-observer');
const buttons = container.querySelector('.buttons');
const button = container.querySelector('sl-button[type="primary"]');
let i = 0;
// Add a button
button.addEventListener('click', () => {
const button = document.createElement('sl-button');
button.textContent = ++i;
buttons.append(button);
});
// Remove a button
buttons.addEventListener('click', event => {
const target = event.target.closest('sl-button:not([type="primary"])');
event.stopPropagation();
if (target) {
target.remove();
}
});
// Log mutations
mutationObserver.addEventListener('sl-mutation', event => {
console.log(event.detail);
});
</script>
<style>
.mutation-child-list .buttons {
display: flex;
gap: .25rem;
flex-wrap: wrap;
margin-bottom: 1rem;
}
</style>
</div>
```
[component-metadata:sl-mutation-observer]

View File

@@ -5,7 +5,7 @@
Progress bars are used to show the status of an ongoing operation.
```html preview
<sl-progress-bar percentage="50"></sl-progress-bar>
<sl-progress-bar value="50"></sl-progress-bar>
```
## Examples
@@ -15,15 +15,23 @@ Progress bars are used to show the status of an ongoing operation.
Use the `--height` custom property to set the progress bar's height.
```html preview
<sl-progress-bar percentage="50" style="--height: 6px;"></sl-progress-bar>
<sl-progress-bar value="50" style="--height: 6px;"></sl-progress-bar>
```
### Labels
Use the default slot to show a label.
Use the `label` attribute to label the progress bar and tell assistive devices how to announce it.
```html preview
<sl-progress-bar percentage="50" class="progress-bar-labels">50%</sl-progress-bar>
<sl-progress-bar value="50" label="Upload progress"></sl-progress-bar>
```
### Showing Values
Use the default slot to show a value.
```html preview
<sl-progress-bar value="50" class="progress-bar-values">50%</sl-progress-bar>
<br>
@@ -31,27 +39,27 @@ Use the default slot to show a label.
<sl-button circle><sl-icon name="plus"></sl-icon></sl-button>
<script>
const progressBar = document.querySelector('.progress-bar-labels');
const progressBar = document.querySelector('.progress-bar-values');
const subtractButton = progressBar.nextElementSibling.nextElementSibling;
const addButton = subtractButton.nextElementSibling;
addButton.addEventListener('click', () => {
const percentage = Math.min(100, progressBar.percentage + 10);
progressBar.percentage = percentage;
progressBar.textContent = `${percentage}%`;
const value = Math.min(100, progressBar.value + 10);
progressBar.value = value;
progressBar.textContent = `${value}%`;
});
subtractButton.addEventListener('click', () => {
const percentage = Math.max(0, progressBar.percentage - 10)
progressBar.percentage = percentage;
progressBar.textContent = `${percentage}%`;
const value = Math.max(0, progressBar.value - 10)
progressBar.value = value;
progressBar.textContent = `${value}%`;
});
</script>
```
### Indeterminate
The `indeterminate` attribute can be used to inform the user that the operation is pending, but its status cannot currently be determined. In this state, `percentage` is ignored and the label, if present, will not be shown.
The `indeterminate` attribute can be used to inform the user that the operation is pending, but its status cannot currently be determined. In this state, `value` is ignored and the label, if present, will not be shown.
```html preview
<sl-progress-bar indeterminate></sl-progress-bar>

View File

@@ -5,25 +5,25 @@
Progress rings are used to show the progress of a determinate operation in a circular fashion.
```html preview
<sl-progress-ring percentage="25"></sl-progress-ring>
<sl-progress-ring value="25"></sl-progress-ring>
```
## Examples
### Size
Use the `size` attribute to set the diameter of the progress ring.
Use the `--size` custom property to set the diameter of the progress ring.
```html preview
<sl-progress-ring percentage="50" size="200"></sl-progress-ring>
<sl-progress-ring value="50" style="--size: 200px;"></sl-progress-ring>
```
### Stroke Width
### Track Width
Use the `stroke-width` attribute to set the width of the progress ring's indicator.
Use the `--track-width` custom property to set the width of the progress ring's track.
```html preview
<sl-progress-ring percentage="50" stroke-width="10"></sl-progress-ring>
<sl-progress-ring value="50" style="--track-width: 10px;"></sl-progress-ring>
```
### Colors
@@ -32,20 +32,28 @@ To change the color, use the `--track-color` and `--indicator-color` custom prop
```html preview
<sl-progress-ring
percentage="50"
value="50"
style="
--track-color: rgb(var(--sl-color-cyan-100));
--indicator-color: rgb(var(--sl-color-cyan-600));
--track-color: pink;
--indicator-color: deeppink;
"
></sl-progress-ring>
```
### Labels
Use the `label` attribute to label the progress ring and tell assistive devices how to announce it.
```html preview
<sl-progress-ring value="50" label="Upload progress"></sl-progress-ring>
```
### Showing Values
Use the default slot to show a label.
```html preview
<sl-progress-ring percentage="50" size="200" class="progress-ring-labels" style="margin-bottom: .5rem;">50%</sl-progress-ring>
<sl-progress-ring value="50" class="progress-ring-values" style="margin-bottom: .5rem;">50%</sl-progress-ring>
<br>
@@ -53,20 +61,20 @@ Use the default slot to show a label.
<sl-button circle><sl-icon name="plus"></sl-icon></sl-button>
<script>
const progressRing = document.querySelector('.progress-ring-labels');
const progressRing = document.querySelector('.progress-ring-values');
const subtractButton = progressRing.nextElementSibling.nextElementSibling;
const addButton = subtractButton.nextElementSibling;
addButton.addEventListener('click', () => {
const percentage = Math.min(100, progressRing.percentage + 10);
progressRing.percentage = percentage;
progressRing.textContent = `${percentage}%`;
const value = Math.min(100, progressRing.value + 10);
progressRing.value = value;
progressRing.textContent = `${value}%`;
});
subtractButton.addEventListener('click', () => {
const percentage = Math.max(0, progressRing.percentage - 10)
progressRing.percentage = percentage;
progressRing.textContent = `${percentage}%`;
const value = Math.max(0, progressRing.value - 10)
progressRing.value = value;
progressRing.textContent = `${value}%`;
});
</script>
```

View File

@@ -5,7 +5,7 @@
Ranges allow the user to select a single value within a given range using a slider.
```html preview
<sl-range min="0" max="100" step="1"></sl-range>
<sl-range></sl-range>
```
?> This component doesn't work with standard forms. Use [`<sl-form>`](/components/form) instead.
@@ -36,6 +36,17 @@ To disable the tooltip, set `tooltip` to `none`.
<sl-range min="0" max="100" step="1" tooltip="none"></sl-range>
```
### Custom Track Colors
You can customize the active and inactive portions of the track using the `--track-color-active` and `--track-color-inactive` custom properties.
```html preview
<sl-range style="
--track-color-active: rgb(var(--sl-color-primary-600));
--track-color-inactive: rgb(var(--sl-color-primary-200));
"></sl-range>
```
### Custom Tooltip Formatter
You can change the tooltip's content by setting the `tooltipFormatter` property to a function that accepts the range's value as an argument.

View File

@@ -2,9 +2,9 @@
[component-header:sl-resize-observer]
Resize observers offer a thin, declarative interface to the [`ResizeObserver API`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver).
The Resize Observer component offers a thin, declarative interface to the [`ResizeObserver API`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver).
The resize observer will report changes to the dimensions of the elements it wraps through the `sl-resize` event. When emitted, a collection of [`ResizeObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry) objects will be attached to `event.detail`, containing the target element and information about its dimensions.
The resize observer will report changes to the dimensions of the elements it wraps through the `sl-resize` event. When emitted, a collection of [`ResizeObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry) objects will be attached to `event.detail` that contains the target element and information about its dimensions.
```html preview
<div class="resize-observer-overview">
@@ -20,7 +20,7 @@ The resize observer will report changes to the dimensions of the elements it wra
const resizeObserver = container.querySelector('sl-resize-observer');
resizeObserver.addEventListener('sl-resize', event => {
console.log(event);
console.log(event.detail);
});
</script>

View File

@@ -9,7 +9,7 @@ Selects allow you to choose one or more items from a dropdown menu.
<sl-menu-item value="option-1">Option 1</sl-menu-item>
<sl-menu-item value="option-2">Option 2</sl-menu-item>
<sl-menu-item value="option-3">Option 3</sl-menu-item>
<sl-menu-divider></sl-menu-divider>
<sl-divider></sl-divider>
<sl-menu-item value="option-4">Option 4</sl-menu-item>
<sl-menu-item value="option-5">Option 5</sl-menu-item>
<sl-menu-item value="option-6">Option 6</sl-menu-item>
@@ -44,6 +44,18 @@ Use the `clearable` attribute to make the control clearable.
</sl-select>
```
### Filled Selects
Add the `filled` attribute to draw a filled select.
```html preview
<sl-select filled>
<sl-menu-item value="option-1">Option 1</sl-menu-item>
<sl-menu-item value="option-2">Option 2</sl-menu-item>
<sl-menu-item value="option-3">Option 3</sl-menu-item>
</sl-select>
```
### Pill
Use the `pill` attribute to give selects rounded edges.
@@ -77,7 +89,7 @@ To allow multiple options to be selected, use the `multiple` attribute. It's a g
<sl-menu-item value="option-1">Option 1</sl-menu-item>
<sl-menu-item value="option-2">Option 2</sl-menu-item>
<sl-menu-item value="option-3">Option 3</sl-menu-item>
<sl-menu-divider></sl-menu-divider>
<sl-divider></sl-divider>
<sl-menu-item value="option-4">Option 4</sl-menu-item>
<sl-menu-item value="option-5">Option 5</sl-menu-item>
<sl-menu-item value="option-6">Option 6</sl-menu-item>
@@ -86,7 +98,7 @@ To allow multiple options to be selected, use the `multiple` attribute. It's a g
### Grouping Options
Options can be grouped visually using menu labels and menu dividers.
Options can be grouped visually using menu labels and dividers.
```html preview
<sl-select placeholder="Select one">
@@ -94,7 +106,7 @@ Options can be grouped visually using menu labels and menu dividers.
<sl-menu-item value="option-1">Option 1</sl-menu-item>
<sl-menu-item value="option-2">Option 2</sl-menu-item>
<sl-menu-item value="option-3">Option 3</sl-menu-item>
<sl-menu-divider></sl-menu-divider>
<sl-divider></sl-divider>
<sl-menu-label>Group 2</sl-menu-label>
<sl-menu-item value="option-4">Option 4</sl-menu-item>
<sl-menu-item value="option-5">Option 5</sl-menu-item>

View File

@@ -12,7 +12,7 @@ Spinners are used to show the progress of an indeterminate operation.
### Size
Spinners are sized relative to the current font size. To change their size, set the `font-size` property on the spinner itself or on a parent element as shown below.
Spinners are sized based on the current font size. To change their size, set the `font-size` property on the spinner itself or on a parent element as shown below.
```html preview
<sl-spinner></sl-spinner>
@@ -20,12 +20,12 @@ Spinners are sized relative to the current font size. To change their size, set
<sl-spinner style="font-size: 3rem;"></sl-spinner>
```
### Stroke Width
### Track Width
The width of the spinner can be changed by setting the `--stroke-width` custom property.
The width of the spinner's track can be changed by setting the `--track-width` custom property.
```html preview
<sl-spinner style="font-size: 2rem; --stroke-width: 6px;"></sl-spinner>
<sl-spinner style="font-size: 3rem; --track-width: 6px;"></sl-spinner>
```
### Color
@@ -33,7 +33,7 @@ The width of the spinner can be changed by setting the `--stroke-width` custom p
The spinner's colors can be changed by setting the `--indicator-color` and `--track-color` custom properties.
```html preview
<sl-spinner style="font-size: 2rem; --indicator-color: tomato;"></sl-spinner>
<sl-spinner style="font-size: 3rem; --indicator-color: deeppink; --track-color: pink;"></sl-spinner>
```
[component-metadata:sl-spinner]

View File

@@ -2,7 +2,7 @@
[component-header:sl-tab-panel]
Tab panels are used inside tab groups to display content.
Tab panels are used inside [tab groups](/components/tab-group) to display tabbed content.
```html preview
<sl-tab-group>

View File

@@ -2,7 +2,7 @@
[component-header:sl-tab]
Tabs are used inside tab groups to represent tab panels.
Tabs are used inside [tab groups](/components/tab-group) to represent and activate [tab panels](/components/tab-panel).
```html preview
<sl-tab>Tab</sl-tab>

View File

@@ -34,21 +34,21 @@ Use the `pill` attribute to give tabs rounded edges.
<sl-tag size="large" pill>Large</sl-tag>
```
### Clearable
### Removable
Use the `clearable` attribute to add a clear button to the tag.
Use the `removable` attribute to add a remove button to the tag.
```html preview
<div class="tags-clearable">
<sl-tag size="small" clearable>Small</sl-tag>
<sl-tag size="medium" clearable>Medium</sl-tag>
<sl-tag size="large" clearable>Large</sl-tag>
<div class="tags-removable">
<sl-tag size="small" removable>Small</sl-tag>
<sl-tag size="medium" removable>Medium</sl-tag>
<sl-tag size="large" removable>Large</sl-tag>
</div>
<script>
const div = document.querySelector('.tags-clearable');
const div = document.querySelector('.tags-removable');
div.addEventListener('sl-clear', event => {
div.addEventListener('sl-remove', event => {
const tag = event.target;
tag.style.opacity = '0';
setTimeout(() => tag.style.opacity = '1', 2000);
@@ -56,7 +56,7 @@ Use the `clearable` attribute to add a clear button to the tag.
</script>
<style>
.tags-clearable sl-tag {
.tags-removable sl-tag {
transition: var(--sl-transition-medium) opacity;
}
</style>

View File

@@ -30,9 +30,17 @@ Use the `placeholder` attribute to add a placeholder.
<sl-textarea placeholder="Type something"></sl-textarea>
```
### Filled Textareas
Add the `filled` attribute to draw a filled textarea.
```html preview
<sl-textarea placeholder="Type something" filled></sl-textarea>
```
### Disabled
Use the `disabled` attribute to disable an input.
Use the `disabled` attribute to disable a textarea.
```html preview
<sl-textarea placeholder="Textarea" disabled></sl-textarea>

View File

@@ -165,7 +165,7 @@ To override it globally, set it in a root block in your stylesheet after the Sho
### HTML in Tooltips
Use the `content` slot to create tooltips with HTML content.
Use the `content` slot to create tooltips with HTML content. Tooltips are designed only for text and presentational elements. Avoid placing interactive content, such as buttons, links, and form controls, in a tooltip.
```html preview
<sl-tooltip>
@@ -174,4 +174,29 @@ Use the `content` slot to create tooltips with HTML content.
</sl-tooltip>
```
### Hoisting
Tooltips will be clipped if they're inside a container that has `overflow: auto|hidden|scroll`. The `hoist` attribute forces the tooltip to use a fixed positioning strategy, allowing it to break out of the container. In this case, the tooltip will be positioned relative to its containing block, which is usually the viewport unless an ancestor uses a `transform`, `perspective`, or `filter`. [Refer to this page](https://developer.mozilla.org/en-US/docs/Web/CSS/position#fixed) for more details.
```html preview
<div class="tooltip-hoist">
<sl-tooltip content="This is a tooltip">
<sl-button>No Hoist</sl-button>
</sl-tooltip>
<sl-tooltip content="This is a tooltip" hoist>
<sl-button>Hoist</sl-button>
</sl-tooltip>
</div>
<style>
.tooltip-hoist {
border: solid 2px rgb(var(--sl-panel-border-color));
overflow: hidden;
padding: var(--sl-spacing-medium);
position: relative;
}
</style>
```
[component-metadata:sl-tooltip]

View File

@@ -29,7 +29,7 @@ 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.styles.ts` in the project's [source code](https://github.com/shoelace-style/shoelace/blob/current/src/themes/base.styles.ts).
Many design tokens are described further along in this documentation. For a complete list, refer to `src/themes/light.styles.ts` in the project's [source code](https://github.com/shoelace-style/shoelace/blob/current/src/themes/light.styles.ts).
## Component Parts

View File

@@ -1,10 +1,28 @@
# Installation
You can use Shoelace via CDN or by installing it locally.
You can use Shoelace via CDN or by installing it locally. You can also [cherry pick](#cherry-picking) individual components for faster load times.
## CDN Installation (Recommended)
The easiest way to install Shoelace is with the CDN. Just add the following tags to your page.
The easiest way to install Shoelace is with the CDN. Just add the following tags to your page to get all components and the default light theme.
```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/themes/light.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/shoelace.js"></script>
```
### Dark Theme
If you prefer to use the dark theme instead, use this. Note the `sl-theme-dark` class on the `<html>` element. [Learn more about the Dark Theme.](/getting-started/themes#dark-theme)
```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/themes/dark.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/shoelace.js"></script>
```
### Light & Dark Theme
If you want to load the light or dark theme based on the user's `prefers-color-scheme` setting, use this. The `media` attributes ensure that only the user's preferred theme stylesheet loads and the `onload` attribute sets the appropriate [theme class](/getting-started/themes) on the `<html>` element.
```html
<link rel="stylesheet" media="(prefers-color-scheme:light)" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/themes/light.css">
@@ -14,32 +32,6 @@ The easiest way to install Shoelace is with the CDN. Just add the following tags
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/shoelace.js"></script>
```
The `media` attributes ensure that only the user's preferred theme stylesheet loads, and the `onload` attribute sets the appropriate [theme class](/getting-started/themes/) on the `<html>` element.
### Forcing Light or Dark Themes
To force Shoelace to use the light theme, load the light stylesheet only. Since light theme is the default, you don't need to take any further steps.
```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/themes/light.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/shoelace.js"></script>
```
To force the dark theme, set the `sl-theme-dark` class on the `<html>` element and load the dark stylesheet.
```html
<html class="sl-theme-dark">
<head>
<!-- ... -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/themes/dark.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/dist/shoelace.js"></script>
</head>
<body>
<!-- ... -->
</body>
</html>
```
Now you can [start using Shoelace!](/getting-started/usage)
## Local Installation
@@ -87,12 +79,11 @@ However, if you're [cherry picking](#cherry-picking) or [bundling](#bundling) Sh
The previous approach is the _easiest_ way to load Shoelace, but easy isn't always efficient. You'll incur the full size of the library even if you only use a handful of components. This is convenient for prototyping, but may result in longer load times in production. To improve this, you can cherry pick the components you need.
Cherry picking can be done from your local install or [directly from the CDN](https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/). This will limit the number of files the browser has to download and reduce the amount of bytes being transferred. The disadvantage is that you need to load and register each component manually.
Cherry picking can be done from your local install or [directly from the CDN](https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/). This will limit the number of files the browser has to download and reduce the amount of bytes being transferred. The disadvantage is that you need to load component manually.
Here's an example that loads only the button component. Again, if you're not using a module resolver, you'll need to adjust the path to point to the folder Shoelace is in.
```html
<!-- The base stylesheet is always required -->
<link rel="stylesheet" href="@shoelace-style/shoelace/dist/themes/light.css">
<script type="module" data-shoelace="/path/to/shoelace">
@@ -102,7 +93,7 @@ Here's an example that loads only the button component. Again, if you're not usi
</script>
```
Some components have dependencies that are automatically imported when you cherry pick. If a component has dependencies, they will be listed in the "Dependencies" section of the component's documentation.
You can copy and paste the code to import a component from the "Importing" section of the component's documentation. Note that some components have dependencies that are automatically imported when you cherry pick. If a component has dependencies, they will be listed in the "Dependencies" section of its docs.
!> Never cherry pick components or utilities from `shoelace.js` as this will cause the browser to load the entire library. Instead, cherry pick from specific modules as shown above.

View File

@@ -2,12 +2,12 @@
<div class="splash-start">
<img class="splash-logo" src="/assets/images/wordmark.svg" alt="Shoelace">
**A forward-thinking library of web components.**
# <span hidden>Shoelace:</span> A forward-thinking library of web components.
- Works with all frameworks 🧩
- Works with CDNs 🚛
- Fully customizable with CSS 🎨
- Includes an official dark theme 🌛
- Includes a dark theme 🌛
- Built with accessibility in mind ♿️
- First-party [React wrappers](/getting-started/usage#react)
- Open source 😸
@@ -19,12 +19,12 @@ Designed in New Hampshire by [Cory LaViska](https://twitter.com/claviska).
</div>
</div>
[![jsDelivr](https://data.jsdelivr.com/v1/package/npm/@shoelace-style/shoelace/badge?style=rounded)](https://www.jsdelivr.com/package/npm/@shoelace-style/shoelace)
[![npm](https://img.shields.io/npm/dw/@shoelace-style/shoelace?label=npm)](https://www.npmjs.com/package/@shoelace-style/shoelace)
[![License](https://img.shields.io/badge/license-MIT-232323.svg?style=flat)](https://github.com/shoelace-style/shoelace/blob/next/LICENSE.md)<br>
[![Discord](https://img.shields.io/badge/Discord-Join%20the%20chat-7289da.svg?style=flat&logo=discord)](https://discord.gg/mg8f26C)
[![Twitter](https://img.shields.io/badge/Twitter-Follow-00acee.svg?style=flat&logo=twitter)](https://twitter.com/shoelace_style)
[![Sponsor](https://img.shields.io/badge/Sponsor-%E2%9D%A4-232323.svg?style=flat&logo=github)](https://github.com/sponsors/claviska)
[![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>
[![Discord](https://img.shields.io/badge/Discord-Join%20the%20chat-5965f2.svg?style=flat-square&logo=discord&logoColor=white)](https://discord.gg/mg8f26C)
[![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)
## Quick Start
@@ -93,9 +93,7 @@ If you need to support IE11 or pre-Chromium Edge, this library isn't for you. Al
Shoelace is designed in New Hampshire by [Cory LaViska](https://twitter.com/claviska). It's available under the terms of the MIT license.
Designing, developing, and supporting this library requires a lot of time, effort, and skill. I'd like to keep it open source so everyone can use it, but that doesn't provide me with any income.
**Therefore, if you're using my software to make a profit,** I respectfully ask that you help [fund its development](https://github.com/sponsors/claviska) by becoming a sponsor. There are multiple tiers to choose from with benefits at every level, including prioritized support, bug fixes, feature requests, and advertising.
Designing, developing, and supporting this library requires a lot of time, effort, and skill. If you're using this software to make a profit, I respectfully ask that you help [fund its development](https://github.com/sponsors/claviska) by becoming a sponsor.
👇 Your support is very much appreciated! 👇
@@ -125,4 +123,5 @@ Special thanks to the following projects and individuals that help make Shoelace
- Positioning of menus, tooltips, et al is handled by [Popper.js](https://popper.js.org/)
- Animations are courtesy of [animate.css](https://animate.style/)
- QR codes are generated with [qr-creator](https://github.com/nimiq/qr-creator)
- Search is powered by [Lunr](https://lunrjs.com/)
- The Shoelace logo was designed with a single shoelace by [Adam K Olson](https://twitter.com/adamkolson)

View File

@@ -108,7 +108,9 @@ You will, however, need to maintain your theme more carefully, as new versions o
## Dark Theme
The built-in dark theme uses an inverted + shifted color scale so, if you're using design tokens as intended, you'll get a decent dark mode for free. While this isn't the same as a professionally curated dark theme, it provides an excellent baseline for one and you're encouraged to customize it further depending on your needs.
The built-in dark theme uses an inverted + shifted color scale so, if you're using design tokens as intended, you'll get a decent dark mode for free. While this isn't the same as a professionally curated dark theme, it provides an excellent baseline for one and you're encouraged to customize it depending on your needs.
This was achieved by taking the light theme's [color tokens](/tokens/color) and "flipping" the scale so 100 becomes 900, 200 becomes 800, 300 becomes 700, etc. Next, the luminance of each primitive was increased slightly to avoid true black, a color that is typically undesirable in dark themes. The result is a custom palette that complements the light theme well and makes it easy to offer light and dark variations with minimal effort.
To install the dark theme, add the following to the `<head>` section of your page.

View File

@@ -168,9 +168,14 @@ const MyComponent = (props) => {
Vue [plays nice](https://custom-elements-everywhere.com/#vue) with custom elements. You just have to tell it to ignore Shoelace components. This is pretty easy because they all start with `sl-`.
```js
Vue.config.ignoredElements = [/^sl-/];
import { createApp } from 'vue';
import App from './App.vue';
new Vue({ ... });
const app = createApp(App);
app.config.compilerOptions.isCustomElement = tag => tag.startsWith('sl-');
app.mount('#app');
```
### Binding Complex Data
@@ -197,7 +202,7 @@ If that's too verbose, you can use a custom directive instead.
### Using a Custom Directive
You can use [this utility](https://www.npmjs.com/package/@shoelace-style/vue-sl-model) to add a custom directive to Vue that will work just like `v-model` but for Shoelace components. To install it, use this command.
You can use [this utility](https://www.npmjs.com/package/@shoelace-style/vue-sl-model) to add a custom directive that will work just like `v-model` but for Shoelace components. To install it, use this command.
```bash
npm install @shoelace-style/vue-sl-model
@@ -207,12 +212,15 @@ Next, import the directive and enable it like this.
```js
import ShoelaceModelDirective from '@shoelace-style/vue-sl-model';
import { createApp } from 'vue';
import App from './App.vue';
Vue.config.ignoredElements = [/^sl-/];
Vue.use(ShoelaceModelDirective);
const app = createApp(App);
app.use(ShoelaceModelDirective);
// Your init here
new Vue({ ... });
app.config.compilerOptions.isCustomElement = tag => tag.startsWith('sl-');
app.mount('#app');
```
Now you can use the `v-sl-model` directive to keep your data in sync!

View File

@@ -34,6 +34,7 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsify@4/themes/pure.css" />
<link rel="stylesheet" href="/assets/styles/docs.css" />
<link rel="stylesheet" href="/assets/plugins/code-block/code-block.css" />
<link rel="stylesheet" href="/assets/plugins/search/search.css" />
<link rel="stylesheet" href="/assets/plugins/theme-picker/theme-picker.css" />
<link rel="icon" href="/assets/images/logo.svg" type="image/x-icon" />
<link rel="apple-touch-icon" sizes="180x180" href="/assets/images/touch-icon.png" />
@@ -71,6 +72,7 @@
maxLevel: 3,
subMaxLevel: 2,
name: 'Shoelace',
nameLink: '/',
notFoundPage: '404.md',
pagination: {
previousText: 'Previous',
@@ -79,28 +81,20 @@
crossChapterText: false
},
routerMode: 'history',
search: {
maxAge: 86400000, // Expiration time, the default one day
paths: 'auto',
placeholder: 'Search',
noData: 'No Results',
depth: 3,
namespace: 'shoelace-docs'
},
themeColor: 'rgb(var(--sl-color-primary-500))'
};
</script>
<script src="https://cdn.jsdelivr.net/npm/docsify@4/lib/docsify.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/docsify@4/lib/plugins/search.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/docsify@4/lib/plugins/ga.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/docsify-copy-code@2"></script>
<script src="https://cdn.jsdelivr.net/npm/docsify-pagination@2/dist/docsify-pagination.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.19.0/components/prism-bash.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.19.0/components/prism-jsx.min.js"></script>
<script src="/assets/plugins/code-block/code-block.js"></script>
<script src="/assets/plugins/scroll-position/scroll-position.js"></script>
<script src="/assets/plugins/theme-picker/theme-picker.js"></script>
<script src="/assets/plugins/metadata/metadata.js"></script>
<script src="/assets/plugins/sidebar/sidebar.js"></script>
<script src="/assets/plugins/scroll-position/scroll-position.js"></script>
<script src="/assets/plugins/search/lunr.min.js"></script>
<script src="/assets/plugins/search/search.js"></script>
<script src="/assets/plugins/theme-picker/theme-picker.js"></script>
</body>
</html>

View File

@@ -6,6 +6,104 @@ Components with the <sl-badge type="warning" pill>Experimental</sl-badge> badge
_During the beta period, these restrictions may be relaxed in the event of a mission-critical bug._ 🐛
## Next
- Added experimental `<sl-context-menu>` component
- Added eye dropper to `<sl-color-picker>` when the browser supports the [EyeDropper API](https://wicg.github.io/eyedropper-api/)
- Fixed a bug in `<sl-button-group>` where buttons groups with only one button would have an incorrect border radius
- Improved the `<sl-color-picker>` trigger's border in dark mode
- Refactored positioning logic in `<sl-dropdown>` so Popper is only active when the menu is open
- Updated to Lit 2.0.2
## 2.0.0-beta.58
This version once again restores the bundled distribution because the unbundled + CDN approach is currently confusing and [not working properly](https://github.com/shoelace-style/shoelace/issues/559#issuecomment-949662331). Unbundling the few dependencies Shoelace has is still a goal of the project, but [this jsDelivr bug](https://github.com/jsdelivr/jsdelivr/issues/18337) needs to be resolved before we can achieve it.
I sincerely apologize for the instability of the last few beta releases as a result of this effort.
- Added experimental `<sl-animated-image>` component
- Added `label` attribute to `<sl-progress-bar>` and `<sl-progress-ring>` to improve a11y
- Fixed a bug where the tooltip would show briefly when clicking a disabled `<sl-range>`
- Fixed a bug that caused a console error when `<sl-range>` was used
- Fixed a bug where the `nav` part in `<sl-tab-group>` was on the incorrect element [#563](https://github.com/shoelace-style/shoelace/pull/563)
- Fixed a bug where non-integer aspect ratios were calculated incorrectly in `<sl-responsive-media>`
- Fixed a bug in `<sl-range>` where setting `value` wouldn't update the active and inactive portion of the track [#572](https://github.com/shoelace-style/shoelace/pull/572)
- Reverted to publishing the bundled dist and removed `/+esm` links from the docs
- Updated to Bootstrap Icons to 1.6.1
## 2.0.0-beta.57
- Fix CodePen links and CDN links
## 2.0.0-beta.56
This release is the second attempt at unbundling dependencies. This will be a breaking change only if your configuration _does not_ support bare module specifiers. CDN users and bundler users will be unaffected, but note the URLs for modules on the CDN must have the `/+esm` now.
- Added the `hoist` attribute to `<sl-tooltip>` [#564](https://github.com/shoelace-style/shoelace/issues/564)
- Unbundled dependencies and configured external imports to be packaged with bare module specifiers
## 2.0.0-beta.55
- Revert unbundling due to issues with the CDN not handling bare module specifiers as expected
## 2.0.0-beta.54
Shoelace doesn't have a lot of dependencies, but this release unbundles most of them so you can potentially save some extra kilobytes. This will be a breaking change only if your configuration _does not_ support bare module specifiers. CDN users and bundler users will be unaffected.
- 🚨 BREAKING: renamed the `sl-clear` event to `sl-remove`, the `clear-button` part to `remove-button`, and the `clearable` property to `removable` in `<sl-tag>`
- Added the `disabled` prop to `<sl-resize-observer>`
- Fixed a bug in `<sl-mutation-observer>` where setting `disabled` initially didn't work
- Unbundled dependencies and configured external imports to be packaged with bare module specifiers
## 2.0.0-beta.53
- 🚨 BREAKING: removed `<sl-menu-divider>` (use `<sl-divider>` instead)
- 🚨 BREAKING: removed `percentage` attribute from `<sl-progress-bar>` and `<sl-progress-ring>` (use `value`) instead
- 🚨 BREAKING: switched the default `type` of `<sl-tag>` from `primary` to `neutral`
- Added the experimental `<sl-mutation-observer>` component
- Added the `<sl-divider>` component
- Added `--sl-surface-base` and `--sl-surface-base-alt` as early surface tokens to improve the appearance of alert, card, and panels in dark mode
- Added the `--sl-panel-border-width` design token
- Added missing background color to `<sl-details>`
- Added the `--padding` custom property to `<sl-tab-panel>`
- Added the `outline` variation to `<sl-button>` [#522](https://github.com/shoelace-style/shoelace/issues/522)
- Added the `filled` variation to `<sl-input>`, `<sl-textarea>`, and `<sl-select>` and supporting design tokens
- Added the `control` part to `<sl-select>` so you can target the main control with CSS [#538](https://github.com/shoelace-style/shoelace/issues/538)
- Added a border to `<sl-badge>` to improve contrast when drawn on various background colors
- Added `--track-color-active` and `--track-color-inactive` custom properties to `<sl-range>` [#550](https://github.com/shoelace-style/shoelace/issues/550)
- Added the undocumented custom properties `--thumb-size`, `--tooltip-offset`, `--track-height` on `<sl-range>`
- Changed the default `distance` in `<sl-dropdown>` from `2` to `0` [#538](https://github.com/shoelace-style/shoelace/issues/538)
- Fixed a bug where `<sl-select>` would be larger than the viewport when it had lots of options [#544](https://github.com/shoelace-style/shoelace/issues/544)
- Fixed a bug where `<sl-progress-ring>` wouldn't animate in Safari
- Updated the default height of `<sl-progress-bar>` from `16px` to `1rem` and added a subtle shadow to indicate depth
- Removed the `lit-html` dependency and moved corresponding imports to `lit` [#546](https://github.com/shoelace-style/shoelace/issues/546)
## 2.0.0-beta.52
- 🚨 BREAKING: changed the `--stroke-width` custom property of `<sl-spinner>` to `--track-width` for consistency
- 🚨 BREAKING: removed the `size` and `stroke-width` attributes from `<sl-progress-ring>` so it's fully customizable with CSS (use the `--size` and `--track-width` custom properties instead)
- Added the `--speed` custom property to `<sl-spinner>`
- Added the `--size` and `--track-width` custom properties to `<sl-progress-ring>`
- Added tests for `<sl-badge>` [#530](https://github.com/shoelace-style/shoelace/pull/530)
- Fixed a bug where `<sl-tab>` wasn't using a border radius token [#523](https://github.com/shoelace-style/shoelace/issues/523)
- Fixed a bug in the Remix Icons example where some icons would 404 [#528](https://github.com/shoelace-style/shoelace/issues/528)
- Updated `<sl-progress-ring>` to use only CSS for styling
- Updated `<sl-spinner>` to use an SVG and improved the indicator animation
- Updated to Lit 2.0 and lit-html 2.0 🔥
## 2.0.0-beta.51
A number of users had trouble counting characters that repeat, so this release improves design token patterns so that "t-shirt sizes" are more accessible. For example, `--sl-font-size-xxx-large` has become `--sl-font-size-3x-large`. This change applies to all design tokens that use this scale.
- 🚨 BREAKING: all t-shirt size design tokens now use `2x`, `3x`, `4x` instead of `xx`, `xxx`, `xxxx`
- Added missing `--sl-focus-ring-*` tokens to dark theme
- Added an "Importing" section to all components with copy/paste code to make cherry picking easier
- Improved the documentation search with a custom plugin powered by [Lunr](https://lunrjs.com/)
- Improved the `--sl-shadow-x-small` elevation
- Improved visibility of elevations and overlays in dark theme
- Reduced the size of `<sl-color-picker>` slightly to better accommodate mobile devices
- Removed `<sl-icon>` dependency from `<sl-color-picker>` and improved the copy animation
## 2.0.0-beta.50
- Added `<sl-breadcrumb>` and `<sl-breadcrumb-item>` components

View File

@@ -1,6 +1,6 @@
# Community
Shoelace has a budding community of designers and developers that are building amazing things with web components. We'd love for you to become a part of it!
Shoelace has a growing community of designers and developers that are building amazing things with web components. We'd love for you to become a part of it!
Please be respectful of other users and remember that Shoelace is an open source project. We'll try to help when we can, but there's no guarantee we'll be able solve your problem. Please manage your expectations and don't forget to contribute back to the conversation when you can!
@@ -14,6 +14,7 @@ The [discussion forum](https://github.com/shoelace-style/shoelace/discussions) i
- Learn more about the project, its values, and its roadmap
<sl-button type="primary" href="https://github.com/shoelace-style/shoelace/discussions" target="_blank">
<sl-icon name="github" slot="prefix"></sl-icon>
Join the Discussion
</sl-button>
@@ -27,9 +28,19 @@ The [community chat](https://discord.gg/mg8f26C) is open to the public and power
- Chat live with other designers, developers, and Shoelace fans
<sl-button type="primary" href="https://discord.gg/mg8f26C" target="_blank">
<sl-icon name="discord" slot="prefix"></sl-icon>
Join the Chat
</sl-button>
## Stack Overflow
You can post questions on Stack Overflow using [the "shoelace" tag](https://stackoverflow.com/questions/tagged/shoelace). This is a public forum where talented developers answer questions. It's a great way to get help, but it is not maintained or actively monitored by the Shoelace author.
<sl-button type="primary" href="https://stackoverflow.com/questions/ask?tags=shoelace" target="_blank">
<sl-icon name="stack-overflow" slot="prefix"></sl-icon>
Ask for Help
</sl-button>
## Twitter
Follow [@shoelace_style](https://twitter.com/shoelace_style) on Twitter for general updates and announcements about Shoelace. This is a great place to say "hi" or to share something you're working on. You're also welcome to follow [@claviska](https://twitter.com/claviska), the creator, for tweets about web components, web development, and life.
@@ -37,5 +48,6 @@ Follow [@shoelace_style](https://twitter.com/shoelace_style) on Twitter for gene
**Please avoid using Twitter for support questions.** The [discussion forum](https://github.com/shoelace-style/shoelace/discussions) is a much better place to share code snippets, screenshots, and other troubleshooting info. You'll have much better luck there, as more users will have a chance to help you.
<sl-button type="primary" href="https://twitter.com/shoelace_style" target="_blank">
<sl-icon name="twitter" slot="prefix"></sl-icon>
Follow on Twitter
</sl-button>

View File

@@ -4,13 +4,13 @@ Spacing tokens are used to provide consistent spacing between components and con
| Token | Value | Example |
| ------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------- |
| `--sl-spacing-xxx-small` | 0.125rem (2px) | <div class="spacing-demo" style="width: var(--sl-spacing-xxx-small); height: var(--sl-spacing-xxx-small);"></div> |
| `--sl-spacing-xx-small` | 0.25rem (4px) | <div class="spacing-demo" style="width: var(--sl-spacing-xx-small); height: var(--sl-spacing-xx-small);"></div> |
| `--sl-spacing-3x-small` | 0.125rem (2px) | <div class="spacing-demo" style="width: var(--sl-spacing-3x-small); height: var(--sl-spacing-3x-small);"></div> |
| `--sl-spacing-2x-small` | 0.25rem (4px) | <div class="spacing-demo" style="width: var(--sl-spacing-2x-small); height: var(--sl-spacing-2x-small);"></div> |
| `--sl-spacing-x-small` | 0.5rem (8px) | <div class="spacing-demo" style="width: var(--sl-spacing-x-small); height: var(--sl-spacing-x-small);"></div> |
| `--sl-spacing-small` | 0.75rem (12px) | <div class="spacing-demo" style="width: var(--sl-spacing-small); height: var(--sl-spacing-small);"></div> |
| `--sl-spacing-medium` | 1rem (16px) | <div class="spacing-demo" style="width: var(--sl-spacing-medium); height: var(--sl-spacing-medium);"></div> |
| `--sl-spacing-large` | 1.25rem (20px) | <div class="spacing-demo" style="width: var(--sl-spacing-large); height: var(--sl-spacing-large);"></div> |
| `--sl-spacing-x-large` | 1.75rem (28px) | <div class="spacing-demo" style="width: var(--sl-spacing-x-large); height: var(--sl-spacing-x-large);"></div> |
| `--sl-spacing-xx-large` | 2.25rem (36px) | <div class="spacing-demo" style="width: var(--sl-spacing-xx-large); height: var(--sl-spacing-xx-large);"></div> |
| `--sl-spacing-xxx-large` | 3rem (48px) | <div class="spacing-demo" style="width: var(--sl-spacing-xxx-large); height: var(--sl-spacing-xxx-large);"></div> |
| `--sl-spacing-xxxx-large` | 4.5rem (72px) | <div class="spacing-demo" style="width: var(--sl-spacing-xxxx-large); height: var(--sl-spacing-xxxx-large);"></div> |
| `--sl-spacing-2x-large` | 2.25rem (36px) | <div class="spacing-demo" style="width: var(--sl-spacing-2x-large); height: var(--sl-spacing-2x-large);"></div> |
| `--sl-spacing-3x-large` | 3rem (48px) | <div class="spacing-demo" style="width: var(--sl-spacing-3x-large); height: var(--sl-spacing-3x-large);"></div> |
| `--sl-spacing-4x-large` | 4.5rem (72px) | <div class="spacing-demo" style="width: var(--sl-spacing-4x-large); height: var(--sl-spacing-4x-large);"></div> |

View File

@@ -18,15 +18,15 @@ Font sizes use `rem` units so they scale with the base font size. The pixel valu
| Token | Value | Example |
| --------------------------- | --------------- | ----------------------------------------------------------------- |
| `--sl-font-size-xx-small` | 0.625rem (10px) | <span style="font-size: var(--sl-font-size-xx-small)">Aa</span> |
| `--sl-font-size-2x-small` | 0.625rem (10px) | <span style="font-size: var(--sl-font-size-2x-small)">Aa</span> |
| `--sl-font-size-x-small` | 0.75rem (12px) | <span style="font-size: var(--sl-font-size-x-small)">Aa</span> |
| `--sl-font-size-small` | 0.875rem (14px) | <span style="font-size: var(--sl-font-size-small)">Aa</span> |
| `--sl-font-size-medium` | 1rem (16px) | <span style="font-size: var(--sl-font-size-medium)">Aa</span> |
| `--sl-font-size-large` | 1.25rem (20px) | <span style="font-size: var(--sl-font-size-large)">Aa</span> |
| `--sl-font-size-x-large` | 1.5rem (24px) | <span style="font-size: var(--sl-font-size-x-large)">Aa</span> |
| `--sl-font-size-xx-large` | 2.25rem (36px) | <span style="font-size: var(--sl-font-size-xx-large)">Aa</span> |
| `--sl-font-size-xxx-large` | 3rem (48px) | <span style="font-size: var(--sl-font-size-xxx-large)">Aa</span> |
| `--sl-font-size-xxxx-large` | 4.5rem (72px) | <span style="font-size: var(--sl-font-size-xxxx-large)">Aa</span> |
| `--sl-font-size-2x-large` | 2.25rem (36px) | <span style="font-size: var(--sl-font-size-2x-large)">Aa</span> |
| `--sl-font-size-3x-large` | 3rem (48px) | <span style="font-size: var(--sl-font-size-3x-large)">Aa</span> |
| `--sl-font-size-4x-large` | 4.5rem (72px) | <span style="font-size: var(--sl-font-size-4x-large)">Aa</span> |
## Font Weight

View File

@@ -0,0 +1,114 @@
# Integrating with Laravel
This page explains how to integrate Shoelace with a [Laravel](https://laravel.com) app using a local Webpack bundle. This is a community-maintained document. For questions about this integration, please [ask the community](/resources/community).
## Requirements
This integration has been tested with the following:
- Laravel >= 8
- Node >= 14
- Laravel Mix >= 6
## Instructions
These instructions assume an out-of-the-box [Laravel 8+ install](https://laravel.com/docs/8.x/installation) that uses [Laravel Mix](https://laravel.com/docs/8.x/mix) to compile assets.
Be sure to run `npm install` to install the default Laravel front-end dependencies before installing Shoelace.
### Install the Shoelace package
```bash
npm install @shoelace-style/shoelace
```
### Import the Default Theme
Import Shoelace's default theme (stylesheet) in `/resources/css/app.css`:
```css
@import "/node_modules/@shoelace-style/shoelace/dist/themes/light.css";
```
### Import Your Shoelace Components
Import each Shoelace component you plan to use in `/resources/js/boostrap.js`. Since [Laravel Mix](https://laravel.com/docs/8.x/mix) uses Webpack, use the full path to each component -- as outlined in the [Cherry Picking instructions](https://shoelace.style/getting-started/installation?id=cherry-picking). You can find the full import statement for a component in the *Importing* section of the component's documentation (use the *Bundler* import). Your imports should look similar to:
```js
import "@shoelace-style/shoelace/dist/components/button/button.js";
import "@shoelace-style/shoelace/dist/components/dialog/dialog.js";
import "@shoelace-style/shoelace/dist/components/drawer/drawer.js";
import "@shoelace-style/shoelace/dist/components/menu/menu.js";
import "@shoelace-style/shoelace/dist/components/menu-item/menu-item.js";
```
### Set the Base Path
Add the base path to your Shoelace assets (icons, images, etc.) in `/resources/js/boostrap.js`. The path must point to the same folder where you copy assets to in the next step.
```js
import { setBasePath } from "@shoelace-style/shoelace/dist/utilities/base-path.js";
setBasePath("/");
```
Here's an example `/resources/js/boostrap.js` file, after importing and setting the base path and components.
```js
import { setBasePath } from "@shoelace-style/shoelace/dist/utilities/base-path.js";
setBasePath("/assets");
import "@shoelace-style/shoelace/dist/components/button/button.js";
import "@shoelace-style/shoelace/dist/components/dialog/dialog.js";
import "@shoelace-style/shoelace/dist/components/drawer/drawer.js";
import "@shoelace-style/shoelace/dist/components/menu/menu.js";
import "@shoelace-style/shoelace/dist/components/menu-item/menu-item.js";
```
### Configure Laravel Mix
[Laravel Mix](https://laravel.com/docs/8.x/mix) is a wrapper around Webpack that simplifies configuration. Mix is used by default for compiling front-end assets in Laravel.
Modify `webpack.mix.js` to add Shoelace's assets to Webpack's build process:
```js
mix.js("resources/js/app.js", "public/js")
.postCss("resources/css/app.css", "public/css", [])
.copy("node_modules/@shoelace-style/shoelace/dist/assets", "public/assets")
```
Consider [extracting vendor libraries](https://laravel.com/docs/8.x/mix#vendor-extraction) to a separate file. This splits frequently updated vendor libraries (like Shoelace) from your front-end application code -- for better long-term caching.
Here's an example `webpack.mix.js` file that compiles and splits your JS into `app.js` and `vendor.js` files, and builds an optimized CSS bundle using PostCSS.
```js
mix.js("resources/js/app.js", "public/js")
.postCss("resources/css/app.css", "public/css", [])
.copy("node_modules/@shoelace-style/shoelace/dist/assets", "public/assets")
.extract(); // extracts libraries in node_modules to vendor.js
```
### Compile Front-End Assets
Run the [Laravel Mix](https://laravel.com/docs/8.x/mix) npm scripts to build your application's CSS and JavaScript code.
```bash
## build a development bundle
npm run dev
## build a production bundle
npm run prod
```
### Include Front-End Assets in Your Layout File
Most full-stack Laravel applications use [layouts](https://laravel.com/docs/8.x/blade#building-layouts) to define the basic structure of a page.
After compiling your front-end assets (above), include them in your top-level layouts/templates. The following example uses the [Laravel asset helper](https://laravel.com/docs/8.x/helpers#method-asset) to generate a full URL.
```html
<script defer src="{{ asset('js/manifest.js') }}"></script>
<script defer src="{{ asset('js/vendor.js') }}"></script>
<script defer src="{{ asset('/js/app.js') }}"></script>
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
```
Have fun using Shoelace components in your Laravel app!

View File

@@ -23,9 +23,12 @@ yarn add @shoelace-style/shoelace copy-webpack-plugin
The next step is to import Shoelace's default theme (stylesheet) in `app/javascript/stylesheets/application.scss`.
```css
@import '~@shoelace-style/shoelace/dist/themes/base';
@import '~@shoelace-style/shoelace/dist/themes/light';
@import '~@shoelace-style/shoelace/dist/themes/dark'; // Optional dark theme
```
Fore more details about themes, please refer to [Theme Basics](/getting-started/themes?id=theme-basics).
### Importing Required Scripts
After importing the theme, you'll need to import the JavaScript files for Shoelace. Add the following code to `app/javascript/packs/application.js`.

146
package-lock.json generated
View File

@@ -1,19 +1,17 @@
{
"name": "@shoelace-style/shoelace",
"version": "2.0.0-beta.50",
"version": "2.0.0-beta.58",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@shoelace-style/shoelace",
"version": "2.0.0-beta.50",
"version": "2.0.0-beta.58",
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.7.0",
"@shoelace-style/animations": "^1.1.0",
"color": "^3.1.3",
"lit": "^2.0.0-rc.3",
"lit-html": "^2.0.0-rc.4",
"qr-creator": "^1.0.0"
},
"devDependencies": {
@@ -24,10 +22,10 @@
"@web/test-runner": "^0.13.5",
"@web/test-runner-puppeteer": "^0.10.0",
"bluebird": "^3.7.2",
"bootstrap-icons": "^1.4.1",
"bootstrap-icons": "^1.6.1",
"browser-sync": "^2.26.14",
"chalk": "^4.1.0",
"command-line-args": "^5.1.1",
"command-line-args": "^5.2.0",
"comment-parser": "^1.1.5",
"concurrently": "^5.3.0",
"del": "^6.0.0",
@@ -37,6 +35,8 @@
"get-port": "^5.1.1",
"globby": "^11.0.4",
"husky": "^4.3.8",
"lit": "^2.0.2",
"lunr": "^2.3.9",
"mkdirp": "^0.5.5",
"plop": "^2.7.4",
"prettier": "^2.2.1",
@@ -174,9 +174,10 @@
}
},
"node_modules/@lit/reactive-element": {
"version": "1.0.0-rc.2",
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.0.0-rc.2.tgz",
"integrity": "sha512-cujeIl5Ei8FC7UHf4/4Q3bRJOtdTe1vpJV/JEBYCggedmQ+2P8A2oz7eE+Vxi6OJ4nc0X+KZxXnBoH4QrEbmEQ=="
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.0.0.tgz",
"integrity": "sha512-Kpgenb8UNFsKCsFhggiVvUkCbcFQSd6N8hffYEEGjz27/4rw3cTSsmP9t3q1EHOAsdum60Wo64HvuZDFpEwexA==",
"dev": true
},
"node_modules/@mdn/browser-compat-data": {
"version": "3.3.5",
@@ -799,9 +800,10 @@
}
},
"node_modules/@types/trusted-types": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-1.0.6.tgz",
"integrity": "sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw=="
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
"integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
"dev": true
},
"node_modules/@types/uuid": {
"version": "8.3.0",
@@ -1663,9 +1665,9 @@
"dev": true
},
"node_modules/bootstrap-icons": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.5.0.tgz",
"integrity": "sha512-44feMc7DE1Ccpsas/1wioN8ewFJNquvi5FewA06wLnqct7CwMdGDVy41ieHaacogzDqLfG8nADIvMNp9e4bfbA==",
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.6.1.tgz",
"integrity": "sha512-MNpF89+njCdVJePDRbCd2DrUusqIyNsPlBrdKqBEXAvFZpwb+Gc8k2VlyF2ueiDQn1PoeTSg9UqQNgx8tGqHAA==",
"dev": true,
"engines": {
"node": ">=10"
@@ -2425,12 +2427,12 @@
}
},
"node_modules/command-line-args": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.1.1.tgz",
"integrity": "sha512-hL/eG8lrll1Qy1ezvkant+trihbGnaKaeEjj6Scyr3DN+RC7iQ5Rz84IeLERfAWDGo0HBSNAakczwgCilDXnWg==",
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.0.tgz",
"integrity": "sha512-4zqtU1hYsSJzcJBOcNZIbW5Fbk9BkjCp1pZVhQKoRaWL5J7N4XphDLwo8aWwdQpTugxwu+jf9u2ZhkXiqp5Z6A==",
"dev": true,
"dependencies": {
"array-back": "^3.0.1",
"array-back": "^3.1.0",
"find-replace": "^3.0.0",
"lodash.camelcase": "^4.3.0",
"typical": "^4.0.0"
@@ -6144,30 +6146,33 @@
"dev": true
},
"node_modules/lit": {
"version": "2.0.0-rc.3",
"resolved": "https://registry.npmjs.org/lit/-/lit-2.0.0-rc.3.tgz",
"integrity": "sha512-UZDLWuspl7saA+WvS0e+TE3NdGGE05hOIwUPTWiibs34c5QupcEzpjB/aElt79V9bELQVNbUUwa0Ow7D1Wuszw==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/lit/-/lit-2.0.2.tgz",
"integrity": "sha512-hKA/1YaSB+P+DvKWuR2q1Xzy/iayhNrJ3aveD0OQ9CKn6wUjsdnF/7LavDOJsKP/K5jzW/kXsuduPgRvTFrFJw==",
"dev": true,
"dependencies": {
"@lit/reactive-element": "^1.0.0-rc.2",
"lit-element": "^3.0.0-rc.2",
"lit-html": "^2.0.0-rc.4"
"@lit/reactive-element": "^1.0.0",
"lit-element": "^3.0.0",
"lit-html": "^2.0.0"
}
},
"node_modules/lit-element": {
"version": "3.0.0-rc.2",
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.0.0-rc.2.tgz",
"integrity": "sha512-2Z7DabJ3b5K+p5073vFjMODoaWqy5PIaI4y6ADKm+fCGc8OnX9fU9dMoUEBZjFpd/bEFR9PBp050tUtBnT9XTQ==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.0.0.tgz",
"integrity": "sha512-oPqRhhBBhs+AlI62QLwtWQNU/bNK/h2L1jI3IDroqZubo6XVAkyNy2dW3CRfjij8mrNlY7wULOfyyKKOnfEePA==",
"dev": true,
"dependencies": {
"@lit/reactive-element": "^1.0.0-rc.2",
"lit-html": "^2.0.0-rc.3"
"@lit/reactive-element": "^1.0.0",
"lit-html": "^2.0.0"
}
},
"node_modules/lit-html": {
"version": "2.0.0-rc.4",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.0.0-rc.4.tgz",
"integrity": "sha512-WSLGu3vxq7y8q/oOd9I3zxyBELNLLiDk6gAYoKK4PGctI5fbh6lhnO/jVBdy0PV/vTc+cLJCA/occzx3YoNPeg==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.0.0.tgz",
"integrity": "sha512-tJsCapCmc0vtLj6harqd6HfCxnlt/RSkgowtz4SC9dFE3nSL38Tb33I5HMDiyJsRjQZRTgpVsahrnDrR9wg27w==",
"dev": true,
"dependencies": {
"@types/trusted-types": "^1.0.1"
"@types/trusted-types": "^2.0.2"
}
},
"node_modules/localtunnel": {
@@ -6429,6 +6434,12 @@
"node": ">=0.10.0"
}
},
"node_modules/lunr": {
"version": "2.3.9",
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
"integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
"dev": true
},
"node_modules/make-dir": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
@@ -11283,9 +11294,10 @@
}
},
"@lit/reactive-element": {
"version": "1.0.0-rc.2",
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.0.0-rc.2.tgz",
"integrity": "sha512-cujeIl5Ei8FC7UHf4/4Q3bRJOtdTe1vpJV/JEBYCggedmQ+2P8A2oz7eE+Vxi6OJ4nc0X+KZxXnBoH4QrEbmEQ=="
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.0.0.tgz",
"integrity": "sha512-Kpgenb8UNFsKCsFhggiVvUkCbcFQSd6N8hffYEEGjz27/4rw3cTSsmP9t3q1EHOAsdum60Wo64HvuZDFpEwexA==",
"dev": true
},
"@mdn/browser-compat-data": {
"version": "3.3.5",
@@ -11891,9 +11903,10 @@
}
},
"@types/trusted-types": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-1.0.6.tgz",
"integrity": "sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw=="
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
"integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
"dev": true
},
"@types/uuid": {
"version": "8.3.0",
@@ -12566,9 +12579,9 @@
"dev": true
},
"bootstrap-icons": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.5.0.tgz",
"integrity": "sha512-44feMc7DE1Ccpsas/1wioN8ewFJNquvi5FewA06wLnqct7CwMdGDVy41ieHaacogzDqLfG8nADIvMNp9e4bfbA==",
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.6.1.tgz",
"integrity": "sha512-MNpF89+njCdVJePDRbCd2DrUusqIyNsPlBrdKqBEXAvFZpwb+Gc8k2VlyF2ueiDQn1PoeTSg9UqQNgx8tGqHAA==",
"dev": true
},
"brace-expansion": {
@@ -13203,12 +13216,12 @@
}
},
"command-line-args": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.1.1.tgz",
"integrity": "sha512-hL/eG8lrll1Qy1ezvkant+trihbGnaKaeEjj6Scyr3DN+RC7iQ5Rz84IeLERfAWDGo0HBSNAakczwgCilDXnWg==",
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.0.tgz",
"integrity": "sha512-4zqtU1hYsSJzcJBOcNZIbW5Fbk9BkjCp1pZVhQKoRaWL5J7N4XphDLwo8aWwdQpTugxwu+jf9u2ZhkXiqp5Z6A==",
"dev": true,
"requires": {
"array-back": "^3.0.1",
"array-back": "^3.1.0",
"find-replace": "^3.0.0",
"lodash.camelcase": "^4.3.0",
"typical": "^4.0.0"
@@ -16160,30 +16173,33 @@
"dev": true
},
"lit": {
"version": "2.0.0-rc.3",
"resolved": "https://registry.npmjs.org/lit/-/lit-2.0.0-rc.3.tgz",
"integrity": "sha512-UZDLWuspl7saA+WvS0e+TE3NdGGE05hOIwUPTWiibs34c5QupcEzpjB/aElt79V9bELQVNbUUwa0Ow7D1Wuszw==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/lit/-/lit-2.0.2.tgz",
"integrity": "sha512-hKA/1YaSB+P+DvKWuR2q1Xzy/iayhNrJ3aveD0OQ9CKn6wUjsdnF/7LavDOJsKP/K5jzW/kXsuduPgRvTFrFJw==",
"dev": true,
"requires": {
"@lit/reactive-element": "^1.0.0-rc.2",
"lit-element": "^3.0.0-rc.2",
"lit-html": "^2.0.0-rc.4"
"@lit/reactive-element": "^1.0.0",
"lit-element": "^3.0.0",
"lit-html": "^2.0.0"
}
},
"lit-element": {
"version": "3.0.0-rc.2",
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.0.0-rc.2.tgz",
"integrity": "sha512-2Z7DabJ3b5K+p5073vFjMODoaWqy5PIaI4y6ADKm+fCGc8OnX9fU9dMoUEBZjFpd/bEFR9PBp050tUtBnT9XTQ==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.0.0.tgz",
"integrity": "sha512-oPqRhhBBhs+AlI62QLwtWQNU/bNK/h2L1jI3IDroqZubo6XVAkyNy2dW3CRfjij8mrNlY7wULOfyyKKOnfEePA==",
"dev": true,
"requires": {
"@lit/reactive-element": "^1.0.0-rc.2",
"lit-html": "^2.0.0-rc.3"
"@lit/reactive-element": "^1.0.0",
"lit-html": "^2.0.0"
}
},
"lit-html": {
"version": "2.0.0-rc.4",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.0.0-rc.4.tgz",
"integrity": "sha512-WSLGu3vxq7y8q/oOd9I3zxyBELNLLiDk6gAYoKK4PGctI5fbh6lhnO/jVBdy0PV/vTc+cLJCA/occzx3YoNPeg==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.0.0.tgz",
"integrity": "sha512-tJsCapCmc0vtLj6harqd6HfCxnlt/RSkgowtz4SC9dFE3nSL38Tb33I5HMDiyJsRjQZRTgpVsahrnDrR9wg27w==",
"dev": true,
"requires": {
"@types/trusted-types": "^1.0.1"
"@types/trusted-types": "^2.0.2"
}
},
"localtunnel": {
@@ -16394,6 +16410,12 @@
"integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==",
"dev": true
},
"lunr": {
"version": "2.3.9",
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
"integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
"dev": true
},
"make-dir": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",

View File

@@ -1,7 +1,7 @@
{
"name": "@shoelace-style/shoelace",
"description": "A forward-thinking library of web components.",
"version": "2.0.0-beta.50",
"version": "2.0.0-beta.58",
"homepage": "https://github.com/shoelace-style/shoelace",
"author": "Cory LaViska",
"license": "MIT",
@@ -30,8 +30,8 @@
"url": "https://github.com/sponsors/claviska"
},
"scripts": {
"start": "node scripts/build.js --dev",
"build": "node scripts/build.js",
"start": "node scripts/build.js --bundle --serve",
"build": "node scripts/build.js --bundle --types --copydir \"docs/dist\"",
"prepublishOnly": "npm run build && npm run test",
"prettier": "prettier --write --loglevel warn .",
"create": "plop --plopfile scripts/plop/plopfile.cjs",
@@ -42,8 +42,6 @@
"@popperjs/core": "^2.7.0",
"@shoelace-style/animations": "^1.1.0",
"color": "^3.1.3",
"lit": "^2.0.0-rc.3",
"lit-html": "^2.0.0-rc.4",
"qr-creator": "^1.0.0"
},
"devDependencies": {
@@ -54,10 +52,10 @@
"@web/test-runner": "^0.13.5",
"@web/test-runner-puppeteer": "^0.10.0",
"bluebird": "^3.7.2",
"bootstrap-icons": "^1.4.1",
"bootstrap-icons": "^1.6.1",
"browser-sync": "^2.26.14",
"chalk": "^4.1.0",
"command-line-args": "^5.1.1",
"command-line-args": "^5.2.0",
"comment-parser": "^1.1.5",
"concurrently": "^5.3.0",
"del": "^6.0.0",
@@ -67,6 +65,8 @@
"get-port": "^5.1.1",
"globby": "^11.0.4",
"husky": "^4.3.8",
"lit": "^2.0.2",
"lunr": "^2.3.9",
"mkdirp": "^0.5.5",
"plop": "^2.7.4",
"prettier": "^2.2.1",

View File

@@ -1,6 +1,3 @@
//
// Builds the project. To spin up a dev server, pass the --serve flag.
//
import browserSync from 'browser-sync';
import chalk from 'chalk';
import commandLineArgs from 'command-line-args';
@@ -10,51 +7,67 @@ import esbuild from 'esbuild';
import fs from 'fs';
import getPort from 'get-port';
import glob from 'globby';
import mkdirp from 'mkdirp';
import path from 'path';
import { URL } from 'url';
import { execSync } from 'child_process';
const build = esbuild.build;
const bs = browserSync.create();
const { dev } = commandLineArgs({ name: 'dev', type: Boolean });
del.sync('./dist');
const { bundle, copydir, dir, serve, types } = commandLineArgs([
{ name: 'bundle', type: Boolean },
{ name: 'copydir', type: String },
{ name: 'dir', type: String, defaultValue: 'dist' },
{ name: 'serve', type: Boolean },
{ name: 'types', type: Boolean }
]);
try {
if (!dev) execSync('tsc', { stdio: 'inherit' }); // for type declarations
execSync('node scripts/make-metadata.js', { stdio: 'inherit' });
execSync('node scripts/make-vscode-data.js', { stdio: 'inherit' });
execSync('node scripts/make-css.js', { stdio: 'inherit' });
execSync('node scripts/make-icons.js', { stdio: 'inherit' });
} catch (err) {
console.error(chalk.red(err));
process.exit(1);
}
const outdir = dir;
del.sync(outdir);
mkdirp.sync(outdir);
(async () => {
const entryPoints = [
// The whole shebang dist
'./src/shoelace.ts',
// Components
...(await glob('./src/components/**/!(*.(style|test)).ts')),
// Public utilities
...(await glob('./src/utilities/**/!(*.(style|test)).ts')),
// Theme stylesheets
...(await glob('./src/themes/**/!(*.test).ts'))
];
try {
if (types) execSync(`tsc --project . --outdir "${outdir}"`, { stdio: 'inherit' });
execSync(`node scripts/make-metadata.js --outdir "${outdir}"`, { stdio: 'inherit' });
execSync(`node scripts/make-search.js --outdir "${outdir}"`, { stdio: 'inherit' });
execSync(`node scripts/make-vscode-data.js --outdir "${outdir}"`, { stdio: 'inherit' });
execSync(`node scripts/make-css.js --outdir "${outdir}"`, { stdio: 'inherit' });
execSync(`node scripts/make-icons.js --outdir "${outdir}"`, { stdio: 'inherit' });
} catch (err) {
console.error(chalk.red(err));
process.exit(1);
}
const buildResult = await esbuild
.build({
format: 'esm',
target: 'es2017',
entryPoints,
outdir: './dist',
entryPoints: [
// The whole shebang
'./src/shoelace.ts',
// Components
...(await glob('./src/components/**/!(*.(style|test)).ts')),
// Public utilities
...(await glob('./src/utilities/**/!(*.(style|test)).ts')),
// Theme stylesheets
...(await glob('./src/themes/**/!(*.test).ts'))
],
outdir,
chunkNames: 'chunks/[name].[hash]',
incremental: dev,
incremental: serve,
define: {
// Popper.js expects this to be set
'process.env.NODE_ENV': '"production"'
},
bundle: true,
//
// We don't bundle certain dependencies in the unbundled build. This ensures we ship bare module specifiers,
// allowing end users to better optimize when using a bundler. (Only packages that ship ESM can be external.)
//
external: bundle ? undefined : ['@popperjs/core', '@shoelace-style/animations', 'lit', 'qr-creator'],
splitting: true,
plugins: []
})
@@ -63,20 +76,23 @@ try {
process.exit(1);
});
// Create the docs distribution by copying dist into the docs folder. This is what powers the website. It doesn't need
// to exist in dev because Browser Sync routes it virtually.
await del('./docs/dist');
if (!dev) {
await Promise.all([copy('./dist', './docs/dist')]);
// Copy the build output to an additional directory
if (copydir) {
del.sync(copydir);
copy(outdir, copydir);
}
console.log(chalk.green('The build has finished! 📦\n'));
console.log(chalk.green(`The build has been generated at ${outdir} 📦\n`));
if (dev) {
// Dev server
if (serve) {
const port = await getPort({
port: getPort.makeRange(4000, 4999)
});
// Make sure docs/dist is empty since we're serving it virtually
del.sync('docs/dist');
console.log(chalk.cyan(`Launching the Shoelace dev server at http://localhost:${port}! 🥾\n`));
// Launch browser sync
@@ -103,10 +119,10 @@ try {
buildResult
// Rebuild and reload
.rebuild()
.then(async () => {
.then(() => {
// Rebuild stylesheets when a theme file changes
if (/^src\/themes/.test(filename)) {
execSync('node scripts/make-css.js', { stdio: 'inherit' });
execSync(`node scripts/make-css.js --outdir "${outdir}"`, { stdio: 'inherit' });
}
})
.then(() => {
@@ -115,19 +131,22 @@ try {
return;
}
execSync('node scripts/make-metadata.js', { stdio: 'inherit' });
execSync(`node scripts/make-metadata.js --outdir "${outdir}"`, { stdio: 'inherit' });
})
.then(() => {
bs.reload();
})
.then(() => bs.reload())
.catch(err => console.error(chalk.red(err)));
});
// Reload without rebuilding when the docs change
bs.watch(['docs/**/*']).on('change', filename => {
bs.watch(['docs/**/*.md']).on('change', filename => {
console.log(`Docs file changed - ${filename}`);
execSync(`node scripts/make-search.js --outdir "${outdir}"`, { stdio: 'inherit' });
bs.reload();
});
// Cleanup on exit
process.on('SIGTERM', () => buildResult.rebuild.dispose());
}
// Cleanup on exit
process.on('SIGTERM', () => buildResult.rebuild.dispose());
})();

View File

@@ -2,6 +2,7 @@
// This script generates stylesheets from all *.styles.ts files in src/themes
//
import chalk from 'chalk';
import commandLineArgs from 'command-line-args';
import esbuild from 'esbuild';
import fs from 'fs/promises';
import glob from 'globby';
@@ -10,10 +11,13 @@ import path from 'path';
import prettier from 'prettier';
import stripComments from 'strip-css-comments';
const { outdir } = commandLineArgs({ name: 'outdir', type: String });
const files = glob.sync('./src/themes/**/*.styles.ts');
const outdir = './dist/themes';
const themesDir = path.join(outdir, 'themes');
mkdirp.sync(outdir);
console.log('Generating stylesheets');
mkdirp.sync(themesDir);
try {
files.map(async file => {
@@ -30,11 +34,9 @@ try {
const formattedStyles = prettier.format(stripComments(css), { parser: 'css' });
const filename = path.basename(file).replace('.styles.ts', '.css');
const outfile = path.join(outdir, filename);
const outfile = path.join(themesDir, filename);
await fs.writeFile(outfile, formattedStyles, 'utf8');
});
console.log(chalk.cyan(`Successfully generated stylesheets 🎨\n`));
} catch (err) {
console.error(chalk.red('Error generating styleseheets!'));
console.error(err);

View File

@@ -3,6 +3,7 @@
//
import Promise from 'bluebird';
import chalk from 'chalk';
import commandLineArgs from 'command-line-args';
import copy from 'recursive-copy';
import del from 'del';
import download from 'download';
@@ -13,7 +14,9 @@ import { stat, readFile, writeFile } from 'fs/promises';
import glob from 'globby';
import path from 'path';
const iconDir = './dist/assets/icons';
const { outdir } = commandLineArgs({ name: 'outdir', type: String });
const iconDir = path.join(outdir, '/assets/icons');
const iconPackageData = JSON.parse(readFileSync('./node_modules/bootstrap-icons/package.json', 'utf8'));
let numIcons = 0;

View File

@@ -2,12 +2,11 @@
// This script runs the Custom Elements Manifest analyzer to generate custom-elements.json
//
import chalk from 'chalk';
import mkdirp from 'mkdirp';
import commandLineArgs from 'command-line-args';
import { execSync } from 'child_process';
mkdirp.sync('./dist');
const { outdir } = commandLineArgs({ name: 'outdir', type: String });
// Run the analyzer
console.log('Generating component metadata');
execSync('cem analyze --litelement --outdir dist', { stdio: 'inherit' });
console.log(chalk.cyan(`Successfully generated metadata 🏷\n`));
execSync(`cem analyze --litelement --outdir "${outdir}"`, { stdio: 'inherit' });

100
scripts/make-search.js Normal file
View File

@@ -0,0 +1,100 @@
import commandLineArgs from 'command-line-args';
import fs from 'fs';
import path from 'path';
import glob from 'globby';
import lunr from 'lunr';
import { getAllComponents } from './shared.js';
const { outdir } = commandLineArgs({ name: 'outdir', type: String });
const metadata = JSON.parse(fs.readFileSync(path.join(outdir, 'custom-elements.json'), 'utf8'));
console.log('Generating search index for documentation');
(async () => {
function getHeadings(markdown, maxLevel = 6) {
const headings = [];
const lines = markdown.split('\n');
lines.map(line => {
if (line.startsWith('#')) {
const level = line.match(/^(#+)/)[0].length;
const content = line.replace(/^#+/, '');
if (level <= maxLevel) {
headings.push({ level, content });
}
}
});
return headings;
}
function getMembers(markdown) {
const members = [];
const headers = markdown.match(/\[component-header:([a-z-]+)\]/g);
if (!headers) {
return '';
}
headers.map(header => {
const tagName = header.match(/\[component-header:([a-z-]+)\]/)[1];
const component = getAllComponents(metadata).find(component => component.tagName === tagName);
if (component) {
const fields = ['members', 'cssProperties', 'cssParts', 'slots', 'events'];
fields.map(field => {
if (component[field]) {
component[field].map(entry => {
if (entry.name) members.push(entry.name);
if (entry.description) members.push(entry.description);
if (entry.attribute) members.push(entry.attribute);
});
}
});
}
});
return members.join(' ');
}
const files = await glob('./docs/**/*.md');
const map = {};
const searchIndex = lunr(function () {
// The search index uses these field names extensively, so shortening them can save some serious bytes. The initial
// index file went from 468 KB => 401 KB by using single-character names!
this.ref('id'); // id
this.field('t', { boost: 10 }); // title
this.field('h', { boost: 5 }); // headings
this.field('m', { boost: 2 }); // members (props, methods, events, etc.)
this.field('c'); // content
files.map((file, index) => {
const relativePath = path.relative('./docs', file).replace(/\\/g, '/');
const relativePathNoExtension = relativePath.split('.').slice(0, -1).join('.');
const url = relativePath.replace(/\.md$/, '');
const filename = path.basename(file);
// Ignore certain directories and files
if (relativePath.startsWith('assets/') || relativePath.startsWith('dist/') || filename === '_sidebar.md') {
return false;
}
const content = fs.readFileSync(file, 'utf8');
const allHeadings = getHeadings(content, 4);
const title = allHeadings.find(heading => heading.level === 1)?.content || '';
const headings = allHeadings
.filter(heading => heading.level > 1)
.map(heading => heading.content)
.concat([relativePathNoExtension])
.join(' ');
const members = getMembers(content);
this.add({ id: index, t: title, h: headings, m: members, c: content });
map[index] = { title, url };
});
});
fs.writeFileSync('./docs/search.json', JSON.stringify({ searchIndex, map }), 'utf8');
})();

View File

@@ -4,31 +4,17 @@
// You must generate dist/custom-elements.json before running this script.
//
import chalk from 'chalk';
import commandLineArgs from 'command-line-args';
import fs from 'fs';
import path from 'path';
import { getAllComponents } from './shared.js';
const metadata = JSON.parse(fs.readFileSync('./dist/custom-elements.json', 'utf8'));
function getAllComponents() {
const allComponents = [];
metadata.modules.map(module => {
module.declarations?.map(declaration => {
if (declaration.customElement) {
const component = declaration;
if (component) {
allComponents.push(component);
}
}
});
});
return allComponents;
}
const { outdir } = commandLineArgs({ name: 'outdir', type: String });
const metadata = JSON.parse(fs.readFileSync(path.join(outdir, 'custom-elements.json'), 'utf8'));
console.log('Generating custom data for VS Code');
const components = getAllComponents();
const components = getAllComponents(metadata);
const vscode = { tags: [] };
components.map(component => {
@@ -70,6 +56,4 @@ components.map(component => {
vscode.tags.push({ name, attributes });
});
fs.writeFileSync('./dist/vscode.html-custom-data.json', JSON.stringify(vscode, null, 2), 'utf8');
console.log(chalk.cyan(`Successfully generated custom data for VS Code 🔮\n`));
fs.writeFileSync(path.join(outdir, 'vscode.html-custom-data.json'), JSON.stringify(vscode, null, 2), 'utf8');

17
scripts/shared.js Normal file
View File

@@ -0,0 +1,17 @@
export function getAllComponents(metadata) {
const allComponents = [];
metadata.modules.map(module => {
module.declarations?.map(declaration => {
if (declaration.customElement) {
const component = declaration;
if (component) {
allComponents.push(component);
}
}
});
});
return allComponents;
}

View File

@@ -15,9 +15,9 @@ export default css`
position: relative;
display: flex;
align-items: stretch;
background-color: rgb(var(--sl-color-neutral-0));
border: solid 1px rgb(var(--sl-color-neutral-200));
border-top-width: 3px;
background-color: rgb(var(--sl-surface-base-alt));
border: solid var(--sl-panel-border-width) rgb(var(--sl-panel-border-color));
border-top-width: calc(var(--sl-panel-border-width) * 3);
border-radius: var(--sl-border-radius-medium);
box-shadow: var(--box-shadow);
font-family: var(--sl-font-sans);

View File

@@ -1,6 +1,6 @@
import { LitElement, html } from 'lit';
import { customElement, property, query } from 'lit/decorators.js';
import { classMap } from 'lit-html/directives/class-map';
import { classMap } from 'lit/directives/class-map.js';
import { animateTo, stopAnimations } from '../../internal/animate';
import { emit } from '../../internal/event';
import { watch } from '../../internal/watch';
@@ -22,9 +22,9 @@ const toastStack = Object.assign(document.createElement('div'), { className: 'sl
* @slot icon - An icon to show in the alert.
*
* @event sl-show - Emitted when the alert opens.
* @event sl-after-show - Emitted after the alert opens and all transitions are complete.
* @event sl-after-show - Emitted after the alert opens and all animations are complete.
* @event sl-hide - Emitted when the alert closes.
* @event sl-after-hide - Emitted after the alert closes and all transitions are complete.
* @event sl-after-hide - Emitted after the alert closes and all animations are complete.
*
* @csspart base - The component's base wrapper.
* @csspart icon - The container that wraps the alert icon.

View File

@@ -0,0 +1,52 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles';
export default css`
${componentStyles}
:host {
--control-box-size: 2.5rem;
--icon-size: calc(var(--control-box-size) * 0.625);
display: inline-flex;
position: relative;
cursor: pointer;
}
img {
display: block;
width: 100%;
height: 100%;
}
img[aria-hidden='true'] {
display: none;
}
.animated-image__control-box {
display: flex;
position: absolute;
align-items: center;
justify-content: center;
top: calc(50% - var(--control-box-size) / 2);
right: calc(50% - var(--control-box-size) / 2);
width: var(--control-box-size);
height: var(--control-box-size);
font-size: var(--icon-size);
background: none;
border: none;
background-color: rgb(var(--sl-color-neutral-1000) / 50%);
border-radius: var(--sl-border-radius-circle);
color: rgb(var(--sl-color-neutral-0));
pointer-events: none;
transition: var(--sl-transition-fast) opacity;
}
:host([play]:hover) .animated-image__control-box {
opacity: 1;
transform: scale(1);
}
:host([play]:not(:hover)) .animated-image__control-box {
opacity: 0;
}
`;

View File

@@ -0,0 +1,13 @@
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
// import sinon from 'sinon';
import '../../../dist/shoelace.js';
import type SlAnimatedImage from './animated-image';
describe('<sl-animated-image>', () => {
it('should render a component', async () => {
const el = await fixture(html` <sl-animated-image></sl-animated-image> `);
expect(el).to.exist;
});
});

View File

@@ -0,0 +1,120 @@
import { LitElement, html } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { watch } from '../../internal/watch';
import { emit } from '../../internal/event';
import styles from './animated-image.styles';
import '../icon/icon';
/**
* @since 2.0
* @status experimental
*
* @dependency sl-icon
*
* @event sl-load - Emitted when the image loads successfully.
* @event sl-error - Emitted when the image fails to load.
*
* @part - control-box - The container that surrounds the pause/play icons and provides their background.
* @part - play-icon - The icon to use for the play button.
* @part - pause-icon - The icon to use for the pause button.
*
* @cssproperty --control-box-size - The size of the icon box.
* @cssproperty --icon-size - The size of the play/pause icons.
*/
@customElement('sl-animated-image')
export default class SlAnimatedImage extends LitElement {
static styles = styles;
@state() frozenFrame: string;
@state() isLoaded = false;
@query('.animated-image__animated') animatedImage: HTMLImageElement;
/** The image's src attribute. */
@property() src: string;
/** The image's alt attribute. */
@property() alt: string;
/** When set, the image will animate. Otherwise, it will be paused. */
@property({ type: Boolean, reflect: true }) play: boolean;
handleClick() {
this.play = !this.play;
}
handleLoad() {
const canvas = document.createElement('canvas');
const { width, height } = this.animatedImage;
canvas.width = width;
canvas.height = height;
canvas.getContext('2d')!.drawImage(this.animatedImage, 0, 0, width, height);
this.frozenFrame = canvas.toDataURL('image/gif');
if (!this.isLoaded) {
emit(this, 'sl-load');
this.isLoaded = true;
}
}
handleError() {
emit(this, 'sl-error');
}
@watch('play')
async handlePlayChange() {
// When the animation starts playing, reset the src so it plays from the beginning. Since the src is cached, this
// won't trigger another request.
if (this.play) {
this.animatedImage.src = '';
this.animatedImage.src = this.src;
}
}
@watch('src')
handleSrcChange() {
this.isLoaded = false;
}
render() {
return html`
<div class="animated-image">
<img
class="animated-image__animated"
src=${this.src}
alt=${this.alt}
crossorigin="anonymous"
aria-hidden=${this.play ? 'false' : 'true'}
@click=${this.handleClick}
@load=${this.handleLoad}
@error=${this.handleError}
/>
${this.isLoaded
? html`
<img
class="animated-image__frozen"
src=${this.frozenFrame}
alt=${this.alt}
aria-hidden=${this.play ? 'true' : 'false'}
@click=${this.handleClick}
/>
<div part="control-box" class="animated-image__control-box">
${this.play
? html`<sl-icon part="pause-icon" name="pause-fill" library="system"></sl-icon>`
: html`<sl-icon part="play-icon" name="play-fill" library="system"></sl-icon>`}
</div>
`
: ''}
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'sl-animated-image': SlAnimatedImage;
}
}

View File

@@ -0,0 +1,126 @@
import { expect, fixture, html } from '@open-wc/testing';
import '../../../dist/shoelace.js';
import type SlAvatar from './avatar';
describe('<sl-avatar>', () => {
let el: SlAvatar;
describe('when provided no parameters', async () => {
before(async () => {
el = await fixture<SlAvatar>(html` <sl-avatar></sl-avatar> `);
});
it('passes accessibility test', async () => {
await expect(el).to.be.accessible();
});
it('should default to circle styling', async () => {
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
expect(el.getAttribute('shape')).to.eq('circle');
expect(part.classList.value.trim()).to.eq('avatar avatar--circle');
});
});
describe('when provided an image and alt parameter', async () => {
const image =
'https://images.unsplash.com/photo-1529778873920-4da4926a72c2?ixlib=rb-1.2.1&auto=format&fit=crop&w=300&q=80';
const alt = 'Gray tabby kitten looking down';
before(async () => {
el = await fixture<SlAvatar>(html`<sl-avatar image="${image}" alt="${alt}"></sl-avatar>`);
});
it('passes accessibility test', async () => {
/**
* The image element itself is ancillary, because it's parent container contains the
* aria-label which dictates what "sl-avatar" is. This also implies that alt text will
* resolve to "" when not provided and ignored by readers. This is why we use alt="" on
* the image element to pass accessibility.
* https://html.spec.whatwg.org/multipage/images.html#ancillary-images
*/
await expect(el).to.be.accessible();
});
it('renders "image" part, with src and a role of presentation', async () => {
const part = el.shadowRoot?.querySelector('[part="image"]') as HTMLImageElement;
expect(part.getAttribute('src')).to.eq(image);
});
it('renders the alt attribute in the "base" part', async () => {
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
expect(part.getAttribute('aria-label')).to.eq(alt);
});
describe('when an error occurs when attempting to load the image', async () => {
before(async () => {
el = await fixture<SlAvatar>(
html`<sl-avatar image="data:text/plain;not-an-image-url" alt="${alt}"></sl-avatar>`
);
});
it('does not render the "image" part', async () => {
const part = el.shadowRoot?.querySelector('[part="image"]') as HTMLImageElement;
expect(part).not.to.exist;
});
});
});
describe('when provided initials parameter', async () => {
const initials = 'SL';
before(async () => {
el = await fixture<SlAvatar>(html`<sl-avatar initials="${initials}"></sl-avatar>`);
});
it('passes accessibility test', async () => {
await expect(el).to.be.accessible();
});
it('renders "initials" part, with initials as the text node', async () => {
const part = el.shadowRoot?.querySelector('[part="initials"]') as HTMLImageElement;
expect(part.innerText).to.eq(initials);
});
});
['square', 'rounded', 'circle'].forEach(shape => {
describe(`when passed a shape attribute ${shape}`, () => {
before(async () => {
el = await fixture<SlAvatar>(html`<sl-avatar shape="${shape}"></sl-avatar>`);
});
it('passes accessibility test', async () => {
await expect(el).to.be.accessible();
});
it('appends the appropriate class on the "base" part', async () => {
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
expect(el.getAttribute('shape')).to.eq(shape);
expect(part.classList.value.trim()).to.eq(`avatar avatar--${shape}`);
});
});
});
describe('when passed a <span>, on slot "icon"', async () => {
before(async () => {
el = await fixture<SlAvatar>(html`<sl-avatar><span slot="icon">random content</span></sl-avatar>`);
});
it('passes accessibility test', async () => {
await expect(el).to.be.accessible();
});
it('should accept as an assigned child in the shadow root', async () => {
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=icon]');
const childNodes = slot.assignedNodes({ flatten: true });
expect(childNodes.length).to.eq(1);
const span = <HTMLElement>childNodes[0];
expect(span.innerHTML).to.eq('random content');
});
});
});

View File

@@ -1,6 +1,6 @@
import { LitElement, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { classMap } from 'lit-html/directives/class-map';
import { classMap } from 'lit/directives/class-map.js';
import styles from './avatar.styles';
import '../icon/icon';
@@ -61,7 +61,13 @@ export default class SlAvatar extends LitElement {
`}
${this.image && !this.hasError
? html`
<img part="image" class="avatar__image" src="${this.image}" @error="${() => (this.hasError = true)}" />
<img
part="image"
class="avatar__image"
src="${this.image}"
alt=""
@error="${() => (this.hasError = true)}"
/>
`
: ''}
</div>

View File

@@ -17,6 +17,7 @@ export default css`
letter-spacing: var(--sl-letter-spacing-normal);
line-height: 1;
border-radius: var(--sl-border-radius-small);
border: solid 1px rgb(var(--sl-color-neutral-0));
white-space: nowrap;
padding: 3px 6px;
user-select: none;

View File

@@ -0,0 +1,77 @@
import { expect, fixture, html } from '@open-wc/testing';
import '../../../dist/shoelace.js';
import type SlBadge from './badge';
describe('<sl-badge>', () => {
let el: SlBadge;
describe('when provided no parameters', async () => {
before(async () => {
el = await fixture<SlBadge>(html` <sl-badge>Badge</sl-badge> `);
});
it('should render a component that passes accessibility test, with a role of status on the base part.', async () => {
await expect(el).to.be.accessible();
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
expect(part.getAttribute('role')).to.eq('status');
});
it('should render the child content provided', async () => {
expect(el.innerText).to.eq('Badge');
});
it('should default to square styling, with the primary color', async () => {
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
expect(part.classList.value.trim()).to.eq('badge badge--primary');
});
});
describe('when provided a pill parameter', async () => {
before(async () => {
el = await fixture<SlBadge>(html` <sl-badge pill>Badge</sl-badge> `);
});
it('should render a component that passes accessibility test', async () => {
await expect(el).to.be.accessible();
});
it('should append the pill class to the classlist to render a pill', async () => {
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
expect(part.classList.value.trim()).to.eq('badge badge--primary badge--pill');
});
});
describe('when provided a pulse parameter', async () => {
before(async () => {
el = await fixture<SlBadge>(html` <sl-badge pulse>Badge</sl-badge> `);
});
it('should render a component that passes accessibility test', async () => {
await expect(el).to.be.accessible();
});
it('should append the pulse class to the classlist to render a pulse', async () => {
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
expect(part.classList.value.trim()).to.eq('badge badge--primary badge--pulse');
});
});
['primary', 'success', 'neutral', 'warning', 'danger'].forEach(type => {
describe(`when passed a type attribute ${type}`, () => {
before(async () => {
el = await fixture<SlBadge>(html`<sl-badge type="${type as any}">Badge</sl-badge>`);
});
it('should render a component that passes accessibility test', async () => {
await expect(el).to.be.accessible();
});
it('should default to square styling, with the primary color', async () => {
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
expect(part.classList.value.trim()).to.eq(`badge badge--${type}`);
});
});
});
});

View File

@@ -1,6 +1,6 @@
import { LitElement, html } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { classMap } from 'lit-html/directives/class-map';
import { classMap } from 'lit/directives/class-map.js';
import styles from './badge.styles';
/**

View File

@@ -1,13 +1,160 @@
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
// import sinon from 'sinon';
import { expect, fixture, html } from '@open-wc/testing';
import '../../../dist/shoelace.js';
import type SlBreadcrumbItem from './breadcrumb-item';
describe('<sl-breadcrumb-item>', () => {
it('should render a component', async () => {
const el = await fixture(html` <sl-breadcrumb-item></sl-breadcrumb-item> `);
let el: SlBreadcrumbItem;
expect(el).to.exist;
describe('when not provided a href attribute', async () => {
before(async () => {
el = await fixture<SlBreadcrumbItem>(html` <sl-breadcrumb-item>Home</sl-breadcrumb-item> `);
});
it('should render a component that passes accessibility test', async () => {
await expect(el).to.be.accessible();
});
it('should hide the seperator from screen readers', async () => {
const separator: HTMLSpanElement = el.shadowRoot.querySelector('[part="separator"]');
expect(separator).attribute('aria-hidden', 'true');
});
it('should render a HTMLButtonElement as the part "label", with a set type "button"', () => {
const button: HTMLButtonElement = el.shadowRoot.querySelector('[part="label"]');
expect(button).to.exist;
expect(button).attribute('type', 'button');
});
});
describe('when provided a href attribute', async () => {
describe('and no target', () => {
before(async () => {
el = await fixture<SlBreadcrumbItem>(html`
<sl-breadcrumb-item href="https://jsonplaceholder.typicode.com/">Home</sl-breadcrumb-item>
`);
});
it('should render a component that passes accessibility test', async () => {
await expect(el).to.be.accessible();
});
it('should render a HTMLAnchorElement as the part "label", with the supplied href value', () => {
const hyperlink: HTMLAnchorElement = el.shadowRoot.querySelector('[part="label"]');
expect(hyperlink).attribute('href', 'https://jsonplaceholder.typicode.com/');
});
});
describe('and target, without rel', () => {
before(async () => {
el = await fixture<SlBreadcrumbItem>(html`
<sl-breadcrumb-item href="https://jsonplaceholder.typicode.com/" target="_blank">Help</sl-breadcrumb-item>
`);
});
it('should render a component that passes accessibility test', async () => {
await expect(el).to.be.accessible();
});
describe('should render a HTMLAnchorElement as the part "label"', () => {
let hyperlink: HTMLAnchorElement;
before(() => {
hyperlink = el.shadowRoot.querySelector('[part="label"]');
});
it('should use the supplied href value, as the href attribute value', () => {
expect(hyperlink).attribute('href', 'https://jsonplaceholder.typicode.com/');
});
it('should default rel attribute to "noreferrer noopener"', () => {
expect(hyperlink).attribute('rel', 'noreferrer noopener');
});
});
});
describe('and target, with rel', () => {
before(async () => {
el = await fixture<SlBreadcrumbItem>(html`
<sl-breadcrumb-item href="https://jsonplaceholder.typicode.com/" target="_blank" rel="alternate"
>Help</sl-breadcrumb-item
>
`);
});
it('should render a component that passes accessibility test', async () => {
await expect(el).to.be.accessible();
});
describe('should render a HTMLAnchorElement', () => {
let hyperlink: HTMLAnchorElement;
before(() => {
hyperlink = el.shadowRoot.querySelector('a');
});
it('should use the supplied href value, as the href attribute value', () => {
expect(hyperlink).attribute('href', 'https://jsonplaceholder.typicode.com/');
});
it('should use the supplied rel value, as the rel attribute value', () => {
expect(hyperlink).attribute('rel', 'alternate');
});
});
});
});
describe('when provided an element in the slot "prefix" to support prefix icons', async () => {
before(async () => {
el = await fixture<SlBreadcrumbItem>(html`
<sl-breadcrumb-item>
<span class="prefix-example" slot="prefix">/</span>
Home
</sl-breadcrumb-item>
`);
});
it('should render a component that passes accessibility test', async () => {
await expect(el).to.be.accessible();
});
it('should accept as an assigned child in the shadow root', () => {
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=prefix]');
const childNodes = slot.assignedNodes({ flatten: true });
expect(childNodes.length).to.eq(1);
});
it('should append class "breadcrumb-item--has-prefix" to "base" part', () => {
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
expect(part.classList.value).to.equal('breadcrumb-item breadcrumb-item--has-prefix');
});
});
describe('when provided an element in the slot "suffix" to support suffix icons', async () => {
before(async () => {
el = await fixture<SlBreadcrumbItem>(html`
<sl-breadcrumb-item>
<span class="prefix-example" slot="suffix">/</span>
Security
</sl-breadcrumb-item>
`);
});
it('should render a component that passes accessibility test', async () => {
await expect(el).to.be.accessible();
});
it('should accept as an assigned child in the shadow root', () => {
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=suffix]');
const childNodes = slot.assignedNodes({ flatten: true });
expect(childNodes.length).to.eq(1);
});
it('should append class "breadcrumb-item--has-suffix" to "base" part', () => {
const part = el.shadowRoot?.querySelector('[part="base"]') as HTMLElement;
expect(part.classList.value).to.equal('breadcrumb-item breadcrumb-item--has-suffix');
});
});
});

View File

@@ -1,7 +1,7 @@
import { LitElement, html } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { classMap } from 'lit-html/directives/class-map';
import { ifDefined } from 'lit-html/directives/if-defined';
import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { hasSlot } from '../../internal/slot';
import styles from './breadcrumb-item.styles';
@@ -28,12 +28,18 @@ export default class SlBreadcrumbItem extends LitElement {
@state() hasPrefix = false;
@state() hasSuffix = false;
/** Optional link to direct the user to when the breadcrumb item is activated. */
/**
* Optional URL to direct the user to when the breadcrumb item is activated. When set, a link will be rendered
* internally. When unset, a button will be rendered instead.
*/
@property() href: string;
/** Tells the browser where to open the link. Only used when `href` is set. */
@property() target: '_blank' | '_parent' | '_self' | '_top';
/** The `rel` attribute to use on the link. Only used when `href` is set. */
@property() rel: string = 'noreferrer noopener';
handleSlotChange() {
this.hasPrefix = hasSlot(this, 'prefix');
this.hasSuffix = hasSlot(this, 'suffix');
@@ -62,7 +68,7 @@ export default class SlBreadcrumbItem extends LitElement {
class="breadcrumb-item__label breadcrumb-item__label--link"
href="${this.href}"
target="${this.target}"
rel=${ifDefined(this.target ? 'noreferrer noopener' : undefined)}
rel=${ifDefined(this.target ? this.rel : undefined)}
>
<slot></slot>
</a>

View File

@@ -1,13 +1,104 @@
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
// import sinon from 'sinon';
import { expect, fixture, html } from '@open-wc/testing';
import '../../../dist/shoelace.js';
import type SlBreadcrumb from './breadcrumb';
describe('<sl-breadcrumb>', () => {
it('should render a component', async () => {
const el = await fixture(html` <sl-breadcrumb></sl-breadcrumb> `);
let el: SlBreadcrumb;
expect(el).to.exist;
describe('when provided a standard list of el-breadcrumb-item children and no parameters', async () => {
before(async () => {
el = await fixture<SlBreadcrumb>(html`
<sl-breadcrumb>
<sl-breadcrumb-item>Catalog</sl-breadcrumb-item>
<sl-breadcrumb-item>Clothing</sl-breadcrumb-item>
<sl-breadcrumb-item>Women's</sl-breadcrumb-item>
<sl-breadcrumb-item>Shirts &amp; Tops</sl-breadcrumb-item>
</sl-breadcrumb>
`);
});
it('should render a component that passes accessibility test', async () => {
await expect(el).to.be.accessible();
});
it('should render sl-icon as separator', async () => {
expect(el.querySelectorAll('sl-icon').length).to.eq(4);
});
it('should attach aria-current "page" on the last breadcrumb item.', async () => {
const breadcrumbItems = el.querySelectorAll('sl-breadcrumb-item');
const lastNode = breadcrumbItems[3];
expect(lastNode).attribute('aria-current', 'page');
});
});
describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "seperator" to support Custom Separators', async () => {
before(async () => {
el = await fixture<SlBreadcrumb>(html`
<sl-breadcrumb>
<span class="replacement-separator" slot="separator">/</span>
<sl-breadcrumb-item>First</sl-breadcrumb-item>
<sl-breadcrumb-item>Second</sl-breadcrumb-item>
<sl-breadcrumb-item>Third</sl-breadcrumb-item>
</sl-breadcrumb>
`);
});
it('should render a component that passes accessibility test', async () => {
await expect(el).to.be.accessible();
});
it('should accept "separator" as an assigned child in the shadow root', async () => {
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=separator]');
const childNodes = slot.assignedNodes({ flatten: true });
expect(childNodes.length).to.eq(1);
});
it('should replace the sl-icon separator with the provided separator', async () => {
expect(el.querySelectorAll('.replacement-separator').length).to.eq(4);
expect(el.querySelectorAll('sl-icon').length).to.eq(0);
});
});
describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "prefix" to support prefix icons', async () => {
before(async () => {
el = await fixture<SlBreadcrumb>(html`
<sl-breadcrumb>
<sl-breadcrumb-item>
<span class="prefix-example" slot="prefix">/</span>
Home
</sl-breadcrumb-item>
<sl-breadcrumb-item>First</sl-breadcrumb-item>
<sl-breadcrumb-item>Second</sl-breadcrumb-item>
<sl-breadcrumb-item>Third</sl-breadcrumb-item>
</sl-breadcrumb>
`);
});
it('should render a component that passes accessibility test', async () => {
await expect(el).to.be.accessible();
});
});
describe('when provided a standard list of el-breadcrumb-item children and an element in the slot "suffix" to support suffix icons', async () => {
before(async () => {
el = await fixture<SlBreadcrumb>(html`
<sl-breadcrumb>
<sl-breadcrumb-item>First</sl-breadcrumb-item>
<sl-breadcrumb-item>Second</sl-breadcrumb-item>
<sl-breadcrumb-item>Third</sl-breadcrumb-item>
<sl-breadcrumb-item>
<span class="prefix-example" slot="suffix">/</span>
Security
</sl-breadcrumb-item>
</sl-breadcrumb>
`);
});
it('should render a component that passes accessibility test', async () => {
await expect(el).to.be.accessible();
});
});
});

View File

@@ -38,13 +38,13 @@ export default css`
outline: none;
}
.button.button--disabled {
.button--disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* When disabled, prevent mouse events from bubbling up */
.button.button--disabled * {
.button--disabled * {
pointer-events: none;
}
@@ -69,160 +69,302 @@ export default css`
*/
/* Default */
.button.button--default {
.button--standard.button--default {
background-color: rgb(var(--sl-color-neutral-0));
border-color: rgb(var(--sl-color-neutral-300));
color: rgb(var(--sl-color-neutral-700));
}
.button.button--default:hover:not(.button--disabled) {
.button--standard.button--default:hover:not(.button--disabled) {
background-color: rgb(var(--sl-color-primary-50));
border-color: rgb(var(--sl-color-primary-300));
color: rgb(var(--sl-color-primary-700));
}
.button.button--default${focusVisibleSelector}:not(.button--disabled) {
.button--standard.button--default${focusVisibleSelector}:not(.button--disabled) {
background-color: rgb(var(--sl-color-primary-50));
border-color: rgb(var(--sl-color-primary-400));
color: rgb(var(--sl-color-primary-700));
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-primary-500) / var(--sl-focus-ring-alpha));
}
.button.button--default:active:not(.button--disabled) {
.button--standard.button--default:active:not(.button--disabled) {
background-color: rgb(var(--sl-color-primary-100));
border-color: rgb(var(--sl-color-primary-400));
color: rgb(var(--sl-color-primary-700));
}
/* Primary */
.button.button--primary {
.button--standard.button--primary {
background-color: rgb(var(--sl-color-primary-600));
border-color: rgb(var(--sl-color-primary-600));
color: rgb(var(--sl-color-neutral-0));
}
.button.button--primary:hover:not(.button--disabled) {
.button--standard.button--primary:hover:not(.button--disabled) {
background-color: rgb(var(--sl-color-primary-500));
border-color: rgb(var(--sl-color-primary-500));
color: rgb(var(--sl-color-neutral-0));
}
.button.button--primary${focusVisibleSelector}:not(.button--disabled) {
.button--standard.button--primary${focusVisibleSelector}:not(.button--disabled) {
background-color: rgb(var(--sl-color-primary-500));
border-color: rgb(var(--sl-color-primary-500));
color: rgb(var(--sl-color-neutral-0));
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-primary-500) / var(--sl-focus-ring-alpha));
}
.button.button--primary:active:not(.button--disabled) {
.button--standard.button--primary:active:not(.button--disabled) {
background-color: rgb(var(--sl-color-primary-600));
border-color: rgb(var(--sl-color-primary-600));
color: rgb(var(--sl-color-neutral-0));
}
/* Success */
.button.button--success {
.button--standard.button--success {
background-color: rgb(var(--sl-color-success-600));
border-color: rgb(var(--sl-color-success-600));
color: rgb(var(--sl-color-neutral-0));
}
.button.button--success:hover:not(.button--disabled) {
.button--standard.button--success:hover:not(.button--disabled) {
background-color: rgb(var(--sl-color-success-500));
border-color: rgb(var(--sl-color-success-500));
color: rgb(var(--sl-color-neutral-0));
}
.button.button--success${focusVisibleSelector}:not(.button--disabled) {
.button--standard.button--success${focusVisibleSelector}:not(.button--disabled) {
background-color: rgb(var(--sl-color-success-600));
border-color: rgb(var(--sl-color-success-600));
color: rgb(var(--sl-color-neutral-0));
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-success-500) / var(--sl-focus-ring-alpha));
}
.button.button--success:active:not(.button--disabled) {
.button--standard.button--success:active:not(.button--disabled) {
background-color: rgb(var(--sl-color-success-600));
border-color: rgb(var(--sl-color-success-600));
color: rgb(var(--sl-color-neutral-0));
}
/* Neutral */
.button.button--neutral {
.button--standard.button--neutral {
background-color: rgb(var(--sl-color-neutral-600));
border-color: rgb(var(--sl-color-neutral-600));
color: rgb(var(--sl-color-neutral-0));
}
.button.button--neutral:hover:not(.button--disabled) {
.button--standard.button--neutral:hover:not(.button--disabled) {
background-color: rgb(var(--sl-color-neutral-500));
border-color: rgb(var(--sl-color-neutral-500));
color: rgb(var(--sl-color-neutral-0));
}
.button.button--neutral${focusVisibleSelector}:not(.button--disabled) {
.button--standard.button--neutral${focusVisibleSelector}:not(.button--disabled) {
background-color: rgb(var(--sl-color-neutral-500));
border-color: rgb(var(--sl-color-neutral-500));
color: rgb(var(--sl-color-neutral-0));
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-neutral-500) / var(--sl-focus-ring-alpha));
}
.button.button--neutral:active:not(.button--disabled) {
.button--standard.button--neutral:active:not(.button--disabled) {
background-color: rgb(var(--sl-color-neutral-600));
border-color: rgb(var(--sl-color-neutral-600));
color: rgb(var(--sl-color-neutral-0));
}
/* Warning */
.button.button--warning {
.button--standard.button--warning {
background-color: rgb(var(--sl-color-warning-600));
border-color: rgb(var(--sl-color-warning-600));
color: rgb(var(--sl-color-neutral-0));
}
.button.button--warning:hover:not(.button--disabled) {
.button--standard.button--warning:hover:not(.button--disabled) {
background-color: rgb(var(--sl-color-warning-500));
border-color: rgb(var(--sl-color-warning-500));
color: rgb(var(--sl-color-neutral-0));
}
.button.button--warning${focusVisibleSelector}:not(.button--disabled) {
.button--standard.button--warning${focusVisibleSelector}:not(.button--disabled) {
background-color: rgb(var(--sl-color-warning-500));
border-color: rgb(var(--sl-color-warning-500));
color: rgb(var(--sl-color-neutral-0));
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-warning-500) / var(--sl-focus-ring-alpha));
}
.button.button--warning:active:not(.button--disabled) {
.button--standard.button--warning:active:not(.button--disabled) {
background-color: rgb(var(--sl-color-warning-600));
border-color: rgb(var(--sl-color-warning-600));
color: rgb(var(--sl-color-neutral-0));
}
/* Danger */
.button.button--danger {
.button--standard.button--danger {
background-color: rgb(var(--sl-color-danger-600));
border-color: rgb(var(--sl-color-danger-600));
color: rgb(var(--sl-color-neutral-0));
}
.button.button--danger:hover:not(.button--disabled) {
.button--standard.button--danger:hover:not(.button--disabled) {
background-color: rgb(var(--sl-color-danger-500));
border-color: rgb(var(--sl-color-danger-500));
color: rgb(var(--sl-color-neutral-0));
}
.button.button--danger${focusVisibleSelector}:not(.button--disabled) {
.button--standard.button--danger${focusVisibleSelector}:not(.button--disabled) {
background-color: rgb(var(--sl-color-danger-500));
border-color: rgb(var(--sl-color-danger-500));
color: rgb(var(--sl-color-neutral-0));
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-danger-500) / var(--sl-focus-ring-alpha));
}
.button.button--danger:active:not(.button--disabled) {
.button--standard.button--danger:active:not(.button--disabled) {
background-color: rgb(var(--sl-color-danger-600));
border-color: rgb(var(--sl-color-danger-600));
color: rgb(var(--sl-color-neutral-0));
}
/*
* Outline buttons
*/
.button--outline {
background: none;
border: solid 1px;
}
/* Default */
.button--outline.button--default {
border-color: rgb(var(--sl-color-neutral-300));
color: rgb(var(--sl-color-neutral-700));
}
.button--outline.button--default:hover:not(.button--disabled) {
border-color: rgb(var(--sl-color-primary-600));
background-color: rgb(var(--sl-color-primary-600));
color: rgb(var(--sl-color-neutral-0));
}
.button--outline.button--default${focusVisibleSelector}:not(.button--disabled) {
border-color: rgb(var(--sl-color-primary-500));
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-primary-500) / var(--sl-focus-ring-alpha));
}
.button--outline.button--default:active:not(.button--disabled) {
border-color: rgb(var(--sl-color-primary-700));
background-color: rgb(var(--sl-color-primary-700));
color: rgb(var(--sl-color-neutral-0));
}
/* Primary */
.button--outline.button--primary {
border-color: rgb(var(--sl-color-primary-600));
color: rgb(var(--sl-color-primary-600));
}
.button--outline.button--primary:hover:not(.button--disabled) {
background-color: rgb(var(--sl-color-primary-600));
color: rgb(var(--sl-color-neutral-0));
}
.button--outline.button--primary${focusVisibleSelector}:not(.button--disabled) {
border-color: rgb(var(--sl-color-primary-500));
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-primary-500) / var(--sl-focus-ring-alpha));
}
.button--outline.button--primary:active:not(.button--disabled) {
border-color: rgb(var(--sl-color-primary-700));
background-color: rgb(var(--sl-color-primary-700));
color: rgb(var(--sl-color-neutral-0));
}
/* Success */
.button--outline.button--success {
border-color: rgb(var(--sl-color-success-600));
color: rgb(var(--sl-color-success-600));
}
.button--outline.button--success:hover:not(.button--disabled) {
background-color: rgb(var(--sl-color-success-600));
color: rgb(var(--sl-color-neutral-0));
}
.button--outline.button--success${focusVisibleSelector}:not(.button--disabled) {
border-color: rgb(var(--sl-color-success-500));
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-success-500) / var(--sl-focus-ring-alpha));
}
.button--outline.button--success:active:not(.button--disabled) {
border-color: rgb(var(--sl-color-success-700));
background-color: rgb(var(--sl-color-success-700));
color: rgb(var(--sl-color-neutral-0));
}
/* Neutral */
.button--outline.button--neutral {
border-color: rgb(var(--sl-color-neutral-600));
color: rgb(var(--sl-color-neutral-600));
}
.button--outline.button--neutral:hover:not(.button--disabled) {
background-color: rgb(var(--sl-color-neutral-600));
color: rgb(var(--sl-color-neutral-0));
}
.button--outline.button--neutral${focusVisibleSelector}:not(.button--disabled) {
border-color: rgb(var(--sl-color-neutral-500));
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-neutral-500) / var(--sl-focus-ring-alpha));
}
.button--outline.button--neutral:active:not(.button--disabled) {
border-color: rgb(var(--sl-color-neutral-700));
background-color: rgb(var(--sl-color-neutral-700));
color: rgb(var(--sl-color-neutral-0));
}
/* Warning */
.button--outline.button--warning {
border-color: rgb(var(--sl-color-warning-600));
color: rgb(var(--sl-color-warning-600));
}
.button--outline.button--warning:hover:not(.button--disabled) {
background-color: rgb(var(--sl-color-warning-600));
color: rgb(var(--sl-color-neutral-0));
}
.button--outline.button--warning${focusVisibleSelector}:not(.button--disabled) {
border-color: rgb(var(--sl-color-warning-500));
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-warning-500) / var(--sl-focus-ring-alpha));
}
.button--outline.button--warning:active:not(.button--disabled) {
border-color: rgb(var(--sl-color-warning-700));
background-color: rgb(var(--sl-color-warning-700));
color: rgb(var(--sl-color-neutral-0));
}
/* Danger */
.button--outline.button--danger {
border-color: rgb(var(--sl-color-danger-600));
color: rgb(var(--sl-color-danger-600));
}
.button--outline.button--danger:hover:not(.button--disabled) {
background-color: rgb(var(--sl-color-danger-600));
color: rgb(var(--sl-color-neutral-0));
}
.button--outline.button--danger${focusVisibleSelector}:not(.button--disabled) {
border-color: rgb(var(--sl-color-danger-500));
box-shadow: 0 0 0 var(--sl-focus-ring-width) rgb(var(--sl-color-danger-500) / var(--sl-focus-ring-alpha));
}
.button--outline.button--danger:active:not(.button--disabled) {
border-color: rgb(var(--sl-color-danger-700));
background-color: rgb(var(--sl-color-danger-700));
color: rgb(var(--sl-color-neutral-0));
}
/*
* Text buttons
*/
@@ -456,7 +598,7 @@ export default css`
* buttons and we style them here instead.
*/
:host(.sl-button-group__button--first) .button {
:host(.sl-button-group__button--first:not(.sl-button-group__button--last)) .button {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
@@ -465,7 +607,7 @@ export default css`
border-radius: 0;
}
:host(.sl-button-group__button--last) .button {
:host(.sl-button-group__button--last:not(.sl-button-group__button--first)) .button {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}

View File

@@ -1,7 +1,7 @@
import { LitElement, html } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit-html/directives/class-map';
import { ifDefined } from 'lit-html/directives/if-defined';
import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { emit } from '../../internal/event';
import { hasSlot } from '../../internal/slot';
import styles from './button.styles';
@@ -54,6 +54,9 @@ export default class SlButton extends LitElement {
/** Draws the button in a loading state. */
@property({ type: Boolean, reflect: true }) loading = false;
/** Draws an outlined button. */
@property({ type: Boolean, reflect: true }) outline = false;
/** Draws a pill-style button with rounded edges. */
@property({ type: Boolean, reflect: true }) pill = false;
@@ -174,6 +177,8 @@ export default class SlButton extends LitElement {
'button--disabled': this.disabled,
'button--focused': this.hasFocus,
'button--loading': this.loading,
'button--standard': !this.outline,
'button--outline': this.outline,
'button--pill': this.pill,
'button--has-label': this.hasLabel,
'button--has-prefix': this.hasPrefix,
@@ -213,6 +218,8 @@ export default class SlButton extends LitElement {
'button--disabled': this.disabled,
'button--focused': this.hasFocus,
'button--loading': this.loading,
'button--standard': !this.outline,
'button--outline': this.outline,
'button--pill': this.pill,
'button--has-label': this.hasLabel,
'button--has-prefix': this.hasPrefix,

View File

@@ -16,7 +16,7 @@ export default css`
.card {
display: flex;
flex-direction: column;
background-color: rgb(var(--sl-color-neutral-0));
background-color: rgb(var(--sl-surface-base-alt));
box-shadow: var(--sl-shadow-x-small);
border: solid var(--border-width) var(--border-color);
border-radius: var(--border-radius);

View File

@@ -0,0 +1,139 @@
import { expect, fixture, html } from '@open-wc/testing';
import '../../../dist/shoelace.js';
import type SlCard from './card';
describe('<sl-card>', () => {
let el: SlCard;
describe('when provided no parameters', async () => {
before(async () => {
el = await fixture<SlCard>(
html` <sl-card>This is just a basic card. No image, no header, and no footer. Just your content.</sl-card> `
);
});
it('should render a component that passes accessibility test.', async () => {
await expect(el).to.be.accessible();
});
it('should render the child content provided.', async () => {
expect(el.innerText).to.eq('This is just a basic card. No image, no header, and no footer. Just your content.');
});
it('should contain the class card.', async () => {
const card = el.shadowRoot.querySelector('.card') as HTMLElement;
expect(card.classList.value.trim()).to.eq('card');
});
});
describe('when provided an element in the slot "header" to render a header', async () => {
before(async () => {
el = await fixture<SlCard>(
html`<sl-card>
<div slot="header">Header Title</div>
This card has a header. You can put all sorts of things in it!
</sl-card>`
);
});
it('should render a component that passes accessibility test.', async () => {
await expect(el).to.be.accessible();
});
it('should render the child content provided.', async () => {
expect(el.innerText).to.contain('This card has a header. You can put all sorts of things in it!');
});
it('render the header content provided.', async () => {
const header = <HTMLDivElement>el.querySelector('div[slot=header]');
expect(header.innerText).eq('Header Title');
});
it('accept "header" as an assigned child in the shadow root.', async () => {
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=header]');
const childNodes = slot.assignedNodes({ flatten: true });
expect(childNodes.length).to.eq(1);
});
it('should contain the class card--has-header.', async () => {
const card = el.shadowRoot.querySelector('.card') as HTMLElement;
expect(card.classList.value.trim()).to.eq('card card--has-header');
});
});
describe('when provided an element in the slot "footer" to render a footer', async () => {
before(async () => {
el = await fixture<SlCard>(
html`<sl-card>
This card has a footer. You can put all sorts of things in it!
<div slot="footer">Footer Content</div>
</sl-card>`
);
});
it('should render a component that passes accessibility test.', async () => {
await expect(el).to.be.accessible();
});
it('should render the child content provided.', async () => {
expect(el.innerText).to.contain('This card has a footer. You can put all sorts of things in it!');
});
it('render the footer content provided.', async () => {
const footer = <HTMLDivElement>el.querySelector('div[slot=footer]');
expect(footer.innerText).eq('Footer Content');
});
it('accept "footer" as an assigned child in the shadow root.', async () => {
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=footer]');
const childNodes = slot.assignedNodes({ flatten: true });
expect(childNodes.length).to.eq(1);
});
it('should contain the class card--has-footer.', async () => {
const card = el.shadowRoot.querySelector('.card') as HTMLElement;
expect(card.classList.value.trim()).to.eq('card card--has-footer');
});
});
describe('when provided an element in the slot "image" to render a image', async () => {
before(async () => {
el = await fixture<SlCard>(
html`<sl-card>
<img
slot="image"
src="https://images.unsplash.com/photo-1547191783-94d5f8f6d8b1?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=400&q=80"
alt="A kitten walks towards camera on top of pallet."
/>
This is a kitten, but not just any kitten. This kitten likes walking along pallets.
</sl-card>`
);
});
it('should render a component that passes accessibility test.', async () => {
await expect(el).to.be.accessible();
});
it('should render the child content provided.', async () => {
expect(el.innerText).to.contain(
'This is a kitten, but not just any kitten. This kitten likes walking along pallets.'
);
});
it('accept "image" as an assigned child in the shadow root.', async () => {
const slot = <HTMLSlotElement>el.shadowRoot.querySelector('slot[name=image]');
const childNodes = slot.assignedNodes({ flatten: true });
expect(childNodes.length).to.eq(1);
});
it('should contain the class card--has-image.', async () => {
const card = el.shadowRoot.querySelector('.card') as HTMLElement;
expect(card.classList.value.trim()).to.eq('card card--has-image');
});
});
});

View File

@@ -1,6 +1,6 @@
import { LitElement, html } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { classMap } from 'lit-html/directives/class-map';
import { classMap } from 'lit/directives/class-map.js';
import { hasSlot } from '../../internal/slot';
import styles from './card.styles';

View File

@@ -1,8 +1,8 @@
import { LitElement, html } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit-html/directives/class-map';
import { ifDefined } from 'lit-html/directives/if-defined';
import { live } from 'lit-html/directives/live';
import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { live } from 'lit/directives/live.js';
import { emit } from '../../internal/event';
import { watch } from '../../internal/watch';
import styles from './checkbox.styles';

View File

@@ -6,11 +6,11 @@ export default css`
${componentStyles}
:host {
--grid-width: 300px;
--grid-height: 220px;
--grid-width: 280px;
--grid-height: 200px;
--grid-handle-size: 16px;
--slider-height: 17px;
--slider-handle-size: 19px;
--slider-height: 15px;
--slider-handle-size: 17px;
--swatch-size: 25px;
display: inline-block;
@@ -28,7 +28,7 @@ export default css`
}
.color-picker--inline {
border: solid 1px rgb(var(--sl-panel-border-color));
border: solid var(--sl-panel-border-width) rgb(var(--sl-panel-border-color));
}
.color-picker__grid {
@@ -165,38 +165,19 @@ export default css`
border: solid 1px rgba(0, 0, 0, 0.125);
}
.color-picker__copy-feedback {
width: calc(var(--sl-input-height-small) / 2);
height: calc(var(--sl-input-height-small) / 2);
color: white;
background-color: rgb(var(--sl-color-neutral-900));
border-radius: var(--sl-border-radius-circle);
opacity: 0;
.color-picker__preview-color--copied {
animation: pulse 0.75s;
}
.color-picker__copy-feedback.color-picker__copy-feedback--visible {
animation: copied 1s;
}
@keyframes copied {
@keyframes pulse {
0% {
transform: scale(0.8);
opacity: 0;
box-shadow: 0 0 0 0 rgb(var(--sl-focus-ring-color));
}
30% {
transform: scale(1.2);
opacity: 1;
}
70% {
transform: scale(1.2);
opacity: 1;
box-shadow: 0 0 0 0.5rem transparent;
}
100% {
transform: scale(1.4);
opacity: 0;
box-shadow: 0 0 0 0 transparent;
}
}
@@ -210,11 +191,14 @@ export default css`
flex: 1 1 auto;
}
.color-picker__user-input sl-button-group {
margin-left: var(--sl-spacing-small);
}
.color-picker__user-input sl-button {
min-width: 3.25rem;
max-width: 3.25rem;
font-size: 1rem;
margin-left: var(--sl-spacing-small);
}
.color-picker__swatches {
@@ -318,7 +302,7 @@ export default css`
height: 100%;
border-radius: inherit;
background-color: currentColor;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.25);
box-shadow: inset 0 0 0 1px rgb(var(--sl-color-neutral-1000) / 25%);
transition: inherit;
}

View File

@@ -1,9 +1,9 @@
import { LitElement, html } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit-html/directives/class-map';
import { ifDefined } from 'lit-html/directives/if-defined';
import { live } from 'lit-html/directives/live';
import { styleMap } from 'lit-html/directives/style-map';
import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { live } from 'lit/directives/live.js';
import { styleMap } from 'lit/directives/style-map.js';
import { emit } from '../../internal/event';
import { watch } from '../../internal/watch';
import { clamp } from '../../internal/math';
@@ -13,17 +13,20 @@ import color from 'color';
import styles from './color-picker.styles';
import '../button/button';
import '../button-group/button-group';
import '../dropdown/dropdown';
import '../icon/icon';
import '../input/input';
const hasEyeDropper = 'EyeDropper' in window;
/**
* @since 2.0
* @status stable
*
* @dependency sl-button
* @dependency sl-button-group
* @dependency sl-dropdown
* @dependency sl-icon
* @dependency sl-input
*
* @event sl-change Emitted when the color picker's value changes.
@@ -40,6 +43,7 @@ import '../input/input';
* @csspart slider-handle - Hue and opacity slider handles.
* @csspart preview - The preview color.
* @csspart input - The text input.
* @csspart eye-dropper-button - The toggle format button's base.
* @csspart format-button - The toggle format button's base.
*
* @cssproperty --grid-width - The width of the color grid.
@@ -65,7 +69,6 @@ export default class SlColorPicker extends LitElement {
@state() private saturation = 100;
@state() private lightness = 100;
@state() private alpha = 100;
@state() private showCopyFeedback = false;
/** The current color. */
@property() value = '#ffffff';
@@ -203,8 +206,12 @@ export default class SlColorPicker extends LitElement {
this.input.select();
document.execCommand('copy');
this.previewButton.focus();
this.showCopyFeedback = true;
this.previewButton.addEventListener('animationend', () => (this.showCopyFeedback = false), { once: true });
// Show copied animation
this.previewButton.classList.add('color-picker__preview-color--copied');
this.previewButton.addEventListener('animationend', () =>
this.previewButton.classList.remove('color-picker__preview-color--copied')
);
}
handleFormatToggle() {
@@ -388,10 +395,6 @@ export default class SlColorPicker extends LitElement {
}
}
handleDropdownAfterHide() {
this.showCopyFeedback = false;
}
normalizeColorString(colorString: string) {
//
// The color module we're using doesn't parse % values for the alpha channel in RGBA and HSLA. It also doesn't parse
@@ -568,6 +571,26 @@ export default class SlColorPicker extends LitElement {
this.isSafeValue = false;
}
handleAfterHide() {
this.previewButton.classList.remove('color-picker__preview-color--copied');
}
handleEyeDropper() {
if (!hasEyeDropper) {
return;
}
// @ts-ignore
const eyeDropper = new EyeDropper();
eyeDropper
.open()
.then((colorSelectionResult: any) => this.setColor(colorSelectionResult.sRGBHex))
.catch(() => {
// The user canceled, do nothing
});
}
@watch('format')
handleFormatChange() {
this.syncValues();
@@ -604,6 +627,7 @@ export default class SlColorPicker extends LitElement {
const x = this.saturation;
const y = 100 - this.lightness;
// TODO - i18n for format, copy, and eye dropper buttons
const colorPicker = html`
<div
part="base"
@@ -706,21 +730,12 @@ export default class SlColorPicker extends LitElement {
type="button"
part="preview"
class="color-picker__preview color-picker__transparent-bg"
aria-label="Copy"
style=${styleMap({
'--preview-color': `hsla(${this.hue}deg, ${this.saturation}%, ${this.lightness}%, ${this.alpha / 100})`
})}
@click=${this.handleCopy}
>
<sl-icon
name="check"
library="system"
class=${classMap({
'color-picker__copy-feedback': true,
'color-picker__copy-feedback--visible': this.showCopyFeedback,
'color-picker__copy-feedback--dark': this.lightness > 50
})}
></sl-icon>
</button>
></button>
</div>
<div class="color-picker__user-input">
@@ -738,13 +753,26 @@ export default class SlColorPicker extends LitElement {
@sl-change=${this.handleInputChange}
></sl-input>
${!this.noFormatToggle
? html`
<sl-button exportparts="base:format-button" @click=${this.handleFormatToggle}>
${this.setLetterCase(this.format)}
</sl-button>
`
: ''}
<sl-button-group>
${!this.noFormatToggle
? html`
<sl-button
aria-label="Change format"
exportparts="base:format-button"
@click=${this.handleFormatToggle}
>
${this.setLetterCase(this.format)}
</sl-button>
`
: ''}
${hasEyeDropper
? html`
<sl-button exportparts="base:eye-dropper-button" @click=${this.handleEyeDropper}>
<sl-icon library="system" name="eyedropper" label="Select a color from the screen"></sl-icon>
</sl-button>
`
: ''}
</sl-button-group>
</div>
${this.swatches
@@ -785,7 +813,7 @@ export default class SlColorPicker extends LitElement {
.containing-element=${this}
?disabled=${this.disabled}
?hoist=${this.hoist}
@sl-after-hide=${this.handleDropdownAfterHide}
@sl-after-hide=${this.handleAfterHide}
>
<button
part="trigger"

View File

@@ -0,0 +1,43 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles';
export default css`
${componentStyles}
:host {
display: contents;
}
::slotted(sl-menu) {
min-width: 180px;
background: rgb(var(--sl-panel-background-color));
border: solid var(--sl-panel-border-width) rgb(var(--sl-panel-border-color));
border-radius: var(--sl-border-radius-medium);
box-shadow: var(--sl-shadow-large);
}
.context-menu {
position: relative;
z-index: var(--sl-z-index-dropdown);
}
.context-menu__locater {
position: absolute;
top: 0;
left: 0;
width: 0;
height: 0;
pointer-events: none;
}
.dropdown__positioner {
position: absolute;
}
.context-menu__menu {
position: relative;
top: 0;
left: 0;
pointer-events: all;
}
`;

View File

@@ -0,0 +1,13 @@
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
// import sinon from 'sinon';
import '../../../dist/shoelace.js';
import type SlContextMenu from './context-menu';
describe('<sl-context-menu>', () => {
it('should render a component', async () => {
const el = await fixture(html` <sl-context-menu></sl-context-menu> `);
expect(el).to.exist;
});
});

View File

@@ -0,0 +1,292 @@
import { LitElement, html } from 'lit';
import { customElement, property, query } from 'lit/decorators.js';
import { emit, waitForEvent } from '../../internal/event';
import { watch } from '../../internal/watch';
import { Instance as PopperInstance, createPopper } from '@popperjs/core/dist/esm';
import { animateTo, stopAnimations } from '../../internal/animate';
import { setDefaultAnimation, getAnimation } from '../../utilities/animation-registry';
import type SlMenu from '../menu/menu';
import styles from './context-menu.styles';
import '../menu/menu';
/**
* @since 2.0
* @status experimental
*
* @dependency sl-menu
*
* @event sl-event-name - Emitted as an example.
*
* @slot - Content that will activate the context menu when right-clicked.
* @slot menu - The menu to show when the context menu is activated, an `<sl-menu>` element.
*
* @event sl-show - Emitted when the context menu opens.
* @event sl-after-show - Emitted after the context menu opens and all animations are complete.
* @event sl-hide - Emitted when the context menu closes.
* @event sl-after-hide - Emitted after the context menu closes and all animations are complete.
*
* @animation contextMenu.show - The animation to use when showing the context menu.
* @animation contextMenu.hide - The animation to use when hiding the context menu.
*/
@customElement('sl-context-menu')
export default class SlContextMenu extends LitElement {
static styles = styles;
@query('.context-menu') wrapper: HTMLElement;
@query('.context-menu__locater') locater: HTMLElement;
@query('.context-menu__menu') menu: HTMLSlotElement;
@query('.context-menu__positioner') positioner: HTMLElement;
private popover: PopperInstance;
/**
* The preferred placement of the context menu. Note that the actual placement may vary as needed to keep the menu
* inside of the viewport.
*/
@property() placement:
| 'top'
| 'top-start'
| 'top-end'
| 'right'
| 'right-start'
| 'right-end'
| 'bottom'
| 'bottom-start'
| 'bottom-end'
| 'left'
| 'left-start'
| 'left-end' = 'bottom-start';
/** Disables the context menu so it won't show when triggered. */
@property({ type: Boolean, reflect: true }) disabled = false;
/** The distance in pixels from which to offset the context menu away from its target. */
@property({ type: Number }) distance = 0;
/** Indicates whether or not the context menu is open. You can use this in lieu of the show/hide methods. */
@property({ type: Boolean, reflect: true }) open = false;
/** The distance in pixels from which to offset the context menu along its target. */
@property({ type: Number }) skidding = 0;
/**
* Enable this option to prevent the menu from being clipped when the component is placed inside a container with
* `overflow: auto|hidden|scroll`.
*/
@property({ type: Boolean }) hoist = false;
connectedCallback() {
super.connectedCallback();
this.handleDocumentKeyDown = this.handleDocumentKeyDown.bind(this);
this.handleDocumentMouseDown = this.handleDocumentMouseDown.bind(this);
}
firstUpdated() {
this.menu.hidden = !this.open;
}
getMenu() {
const slot = this.menu.querySelector('slot')!;
return slot.assignedElements({ flatten: true }).filter(el => el.tagName.toLowerCase() === 'sl-menu')[0] as SlMenu;
}
async handleContextMenu(event: MouseEvent) {
const target = event.target as HTMLElement;
const targetRect = target.getBoundingClientRect();
const wrapperRect = this.wrapper.getBoundingClientRect();
const { offsetX, offsetY } = event;
const x = targetRect.left + offsetX - wrapperRect.left;
const y = targetRect.top + offsetY - wrapperRect.top;
event.preventDefault();
if (this.open) {
await this.hide();
}
this.show(x, y);
}
handleDocumentKeyDown(event: KeyboardEvent) {
const menu = this.getMenu();
const menuItems = menu ? menu.getAllItems() : [];
const firstMenuItem = menuItems[0];
const lastMenuItem = menuItems[menuItems.length - 1];
// Close when escape is pressed
if (event.key === 'Escape') {
this.hide();
return;
}
// Forward key presses that don't originate from the menu to allow keyboard selection and type-to-select
if (menu && !event.composedPath().includes(this.menu)) {
// Focus on a menu item
if (['ArrowDown', 'Home'].includes(event.key) && firstMenuItem) {
event.preventDefault();
const menu = this.getMenu();
menu.setCurrentItem(firstMenuItem);
firstMenuItem.focus();
return;
}
if (['ArrowUp', 'End'].includes(event.key) && lastMenuItem) {
event.preventDefault();
menu.setCurrentItem(lastMenuItem);
lastMenuItem.focus();
return;
}
// Other keys bring focus to the menu and initiate type-to-select behavior
const ignoredKeys = ['Tab', 'Shift', 'Meta', 'Ctrl', 'Alt'];
if (!ignoredKeys.includes(event.key)) {
menu.typeToSelect(event.key);
return;
}
}
}
handleDocumentMouseDown(event: MouseEvent) {
const path = event.composedPath() as Array<EventTarget>;
//
// Close the context menu when clicking outside of it. We use a setTimeout here because mousedown fires before
// contextmenu and, if the menu is already open and the user-right clicks again, we want the menu to re-open in the
// new position instead of closing.
//
setTimeout(() => {
if (this.open && !path.includes(this.menu)) {
this.hide();
return;
}
});
}
handleMenuSelect() {
// Close the context menu when a menu item is selected
this.hide();
}
@watch('open', { waitUntilFirstUpdate: true })
async handleOpenChange() {
if (this.disabled) {
return;
}
if (this.open) {
// Show
emit(this, 'sl-show');
document.addEventListener('keydown', this.handleDocumentKeyDown);
document.addEventListener('mousedown', this.handleDocumentMouseDown);
await stopAnimations(this);
this.popover = createPopper(this.locater, this.positioner, {
placement: this.placement,
strategy: this.hoist ? 'fixed' : 'absolute',
modifiers: [
{
name: 'flip',
options: {
boundary: 'viewport'
}
},
{
name: 'offset',
options: {
offset: [this.skidding, this.distance]
}
}
]
});
this.menu.hidden = false;
const { keyframes, options } = getAnimation(this, 'contextMenu.show');
await animateTo(this.menu, keyframes, options);
emit(this, 'sl-after-show');
} else {
// Hide
emit(this, 'sl-hide');
document.removeEventListener('keydown', this.handleDocumentKeyDown);
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
await stopAnimations(this);
const { keyframes, options } = getAnimation(this, 'contextMenu.hide');
await animateTo(this.menu, keyframes, options);
this.menu.hidden = true;
this.locater.style.top = '0px';
this.locater.style.left = '0px';
this.popover.destroy();
emit(this, 'sl-after-hide');
}
}
/** Shows the context menu */
async show(offsetX?: number, offsetY?: number) {
if (this.open) {
return;
}
this.locater.style.top = `${offsetY || 0}px`;
this.locater.style.left = `${offsetX || 0}px`;
this.open = true;
return waitForEvent(this, 'sl-after-show');
}
/** Hides the dropdown panel */
async hide() {
if (!this.open) {
return;
}
this.open = false;
return waitForEvent(this, 'sl-after-hide');
}
render() {
return html`
<div class="context-menu">
<slot @contextmenu=${this.handleContextMenu}></slot>
<div class="context-menu__locater"></div>
<!-- Position the menu with a wrapper since the popover makes use of translate. This let's us add animations
on the menu without interfering with the position. -->
<div class="context-menu__positioner">
<div class="context-menu__menu" hidden @sl-select=${this.handleMenuSelect}>
<slot name="menu"></slot>
</div>
</div>
</div>
`;
}
}
setDefaultAnimation('contextMenu.show', {
keyframes: [
{ opacity: 0, transform: 'scale(0.9)' },
{ opacity: 1, transform: 'scale(1)' }
],
options: { duration: 50, easing: 'ease' }
});
setDefaultAnimation('contextMenu.hide', {
keyframes: [
{ opacity: 1, transform: 'scale(1)' },
{ opacity: 0, transform: 'scale(0.9)' }
],
options: { duration: 150, easing: 'ease' }
});
declare global {
interface HTMLElementTagNameMap {
'sl-context-menu': SlContextMenu;
}
}

View File

@@ -12,6 +12,7 @@ export default css`
.details {
border: solid 1px rgb(var(--sl-color-neutral-200));
border-radius: var(--sl-border-radius-medium);
background-color: rgb(var(--sl-color-neutral-0));
overflow-anchor: none;
}

View File

@@ -1,6 +1,6 @@
import { LitElement, html } from 'lit';
import { customElement, property, query } from 'lit/decorators.js';
import { classMap } from 'lit-html/directives/class-map';
import { classMap } from 'lit/directives/class-map.js';
import { animateTo, stopAnimations, shimKeyframesHeightAuto } from '../../internal/animate';
import { emit } from '../../internal/event';
import { watch } from '../../internal/watch';
@@ -22,9 +22,9 @@ let id = 0;
* @slot summary - The details' summary. Alternatively, you can use the summary prop.
*
* @event sl-show - Emitted when the details opens.
* @event sl-after-show - Emitted after the details opens and all transitions are complete.
* @event sl-after-show - Emitted after the details opens and all animations are complete.
* @event sl-hide - Emitted when the details closes.
* @event sl-after-hide - Emitted after the details closes and all transitions are complete.
* @event sl-after-hide - Emitted after the details closes and all animations are complete.
*
* @csspart base - The component's base wrapper.
* @csspart header - The summary header.

View File

@@ -30,8 +30,8 @@ export default css`
flex-direction: column;
z-index: 2;
width: var(--width);
max-width: calc(100% - var(--sl-spacing-xx-large));
max-height: calc(100% - var(--sl-spacing-xx-large));
max-width: calc(100% - var(--sl-spacing-2x-large));
max-height: calc(100% - var(--sl-spacing-2x-large));
background-color: rgb(var(--sl-panel-background-color));
border-radius: var(--sl-border-radius-medium);
box-shadow: var(--sl-shadow-x-large);

View File

@@ -1,7 +1,7 @@
import { LitElement, html } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { classMap } from 'lit-html/directives/class-map';
import { ifDefined } from 'lit-html/directives/if-defined';
import { classMap } from 'lit/directives/class-map.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { animateTo, stopAnimations } from '../../internal/animate';
import { emit } from '../../internal/event';
import { watch } from '../../internal/watch';
@@ -30,9 +30,9 @@ let id = 0;
* @slot footer - The dialog's footer, usually one or more buttons representing various options.
*
* @event sl-show - Emitted when the dialog opens.
* @event sl-after-show - Emitted after the dialog opens and all transitions are complete.
* @event sl-after-show - Emitted after the dialog opens and all animations are complete.
* @event sl-hide - Emitted when the dialog closes.
* @event sl-after-hide - Emitted after the dialog closes and all transitions are complete.
* @event sl-after-hide - Emitted after the dialog closes and all animations are complete.
* @event sl-initial-focus - Emitted when the dialog opens and the panel gains focus. Calling `event.preventDefault()`
* will prevent focus and allow you to set it on a different element in the dialog, such as an input or button.
* @event sl-request-close - Emitted when the user attempts to close the dialog by clicking the close button, clicking the

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