Porównaj commity

...

256 Commity
v2.8.0 ... next

Autor SHA1 Wiadomość Data
HappyXiaoAnAn 6092796850
Update zh-tw.ts (#2060)
translation mistake
2024-06-11 10:29:05 -04:00
Cory LaViska 4120988079 Merge branch 'evayde-chore/reduce-build-time' into next 2024-06-10 10:37:03 -04:00
Enrico Gruner 77c482ed16 chore(docs) - reduce build time 2024-06-09 23:53:52 +02:00
Cory LaViska 9399df6e19 adjust small spacing in select 2024-06-03 09:54:11 -04:00
Ahmad Alfy b98deb877e
fix(select) display of option group title (#2046)
Fixes #2044
2024-06-03 09:52:11 -04:00
Konnor Rogers f256d7aa8a
Implement roving tabindex for tabs (#2041)
* implement roving tabindex tabs

* implement roving tabindex tabs

* prettier

* implement roving tabindex tabs

* prettier

* remove test.only

* remove unncessary syncing

* Fix manual tab activations not working with roving tabindex

* prettier

* remove unnecessary extra code

* update changelog

* update changelog

* prettier
2024-05-30 12:49:13 -04:00
Cory LaViska f757d514e4 remove unused 2024-05-28 16:37:29 -04:00
Cory LaViska 07d2144395 remove unused 2024-05-28 16:32:58 -04:00
Cory LaViska c042c8fe34 fix ltrs 2024-05-28 15:28:10 -04:00
Cory LaViska 975115a923 use a more reliable rtl detection 2024-05-28 15:18:43 -04:00
Cory LaViska c31d4f5855 2.15.1 2024-05-21 08:38:00 -04:00
Cory LaViska 75e20e0672 update version 2024-05-21 08:23:53 -04:00
Uaena_Alex_John 7ece400a30
add specified file hint (#2022)
It's not clear In [Vue Guide](https://shoelace.style/frameworks/vue#installation), the user may not know where to apply the theme and base path set, so I add file hint like [React Guide](https://shoelace.style/frameworks/react#installation)
2024-05-20 10:50:44 -04:00
cptKNJO 06a27dd899
fix: registering sl-radio twice (#2016) 2024-05-14 13:29:18 -04:00
Konnor Rogers 9767e84d26
fix empty target in radio-group click (#2009)
* fix empty target in radio-group

* Update src/components/radio-group/radio-group.component.ts

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

* add changelog entry

* prettier

---------

Co-authored-by: Cory LaViska <cory@abeautifulsite.net>
2024-05-10 12:31:00 -04:00
Cory LaViska 8726910160
fixes #1979 (#2008) 2024-05-10 11:13:11 -04:00
Cory LaViska d478ccb2da
fixes #2001 (#2007) 2024-05-10 10:44:02 -04:00
Cory LaViska d94acc6e06
fixes #2005 (#2006) 2024-05-10 10:16:57 -04:00
Fiqri Syah Redha eb42671ef3
locale: add Bahasa Indonesia translation (#2003) 2024-05-06 14:45:28 -04:00
Christian Schilling 3ad6364678
Fixed a bug in <sl-textarea> that may throw errors on disconnectedCallback in test environments (#1985) (#1986) 2024-04-22 11:40:54 -04:00
Konnor Rogers 64996b2d35
add changelog entry for button classes (#1976)
* add changelog entry

* prettier
2024-04-12 12:17:37 -04:00
Susanne Kirchner 0daa5d8dee
Fix invalid css on button style (#1975) 2024-04-12 12:07:48 -04:00
Konnor Rogers 16d5575307
Fix: split panel properly recalculates when going from hidden to shown (#1942)
* fix: split-panel now properly calculates it size when it goes from hidden to being shown.

* chore: add changelog note

* prettier
2024-04-11 14:09:56 -04:00
Konnor Rogers a427433701
fix: scrollbar gutters and dialog scrolling on open (#1967)
* fix: scrollbar gutters and dialog scrolling on open

* prettier

* fix check for current scrollbarGutter property

* prettier
2024-04-11 13:52:41 -04:00
Danny Andrews c6da4f5b14
Update docs for customizing button widths (#1973)
Currently, the docs state that you can set a width attribute to
customize the width of buttons, but no such attribute exists. I've
updated the docs to direct people to set a custom width via CSS through
inline styles or a custom class.
2024-04-11 12:32:54 -05:00
Cory LaViska d0b71adb81
update tooltip styles; fixes #1947 (#1948) 2024-03-28 11:01:21 -04:00
Cory LaViska ae66483671 2.15.0 2024-03-25 14:04:59 -04:00
Cory LaViska 537fd87497 update version 2024-03-25 14:04:56 -04:00
Cory LaViska 1534f47d34 skip for now 2024-03-25 14:04:40 -04:00
Cory LaViska eb08be0fce update tests 2024-03-25 13:36:40 -04:00
Cory LaViska dfc4cb6248 fix toggle 2024-03-25 13:36:35 -04:00
Cory LaViska c1eda83e5b prettier 2024-03-25 13:17:59 -04:00
Cory LaViska 0e5048989d update changelog 2024-03-25 13:16:15 -04:00
Konnor Rogers ff2e0486b4
use data attributes (#1928) 2024-03-25 13:14:50 -04:00
Sebi 4aa5e9c1f2
Fixed Firefox select-test (#1921)
* - added firefox as a test target
- fixed failing firefox select-test

* reverted the firefox test target, since the color-picker test still fails
2024-03-25 11:47:45 -04:00
Cory LaViska 0b7e70bccf update changelog 2024-03-25 11:23:37 -04:00
Alessandro 31f2600816
fix(carousel): synchronize slides after scroll (#1923)
* fix(carousel): synchronize slides after scroll

* chore: leftovers
2024-03-25 11:22:28 -04:00
Cory LaViska f6d5344c44 update changelog 2024-03-25 11:21:32 -04:00
Nic Newdigate 5a89439e14
dropdown: add optional sync property to align popup width to trigger slot element width (#1935)
Co-authored-by: Nic Newdigate <nic.newdigate@vivantio.com>
2024-03-25 11:20:23 -04:00
Cory LaViska 2878957ef5
fix clear button clicks (#1911) 2024-03-25 11:16:32 -04:00
Konnor Rogers cd5a6486da
fix(tree) icons rendering as null (#1922)
* fix icons rendering as null

* prettier

* prettier

* Update docs/pages/resources/changelog.md

---------

Co-authored-by: Cory LaViska <cory@abeautifulsite.net>
2024-03-25 11:15:51 -04:00
Cory LaViska 77d6f27248 update changelog 2024-03-25 11:15:03 -04:00
Konnor Rogers 7a62a87b9b
apply mutator to spritesheets (#1927)
* apply mutator to spritesheets

* prettier

* Update docs/pages/resources/changelog.md

---------

Co-authored-by: Cory LaViska <cory@abeautifulsite.net>
2024-03-25 11:14:04 -04:00
Cory LaViska 0ac61a6a22 prettier 2024-03-25 11:13:01 -04:00
Matt Walkland acf76cf359
Expose spinner part on tree item (#1937)
* Expose spinner part on tree item

* Add spinner__base to exportparts of tree-item
2024-03-25 10:59:50 -04:00
Cory LaViska 3451ec753c add ks banner 2024-03-22 11:44:22 -04:00
cyantree 2a4b3ee2e9
fix form selection when element is detached (#1806) (#1881)
Co-authored-by: cyantree <cyantree@users.noreply.github.com>
2024-03-06 10:34:59 -05:00
Konnor Rogers 3bc8495874
Fixes scroll lock layout shift (#1895)
* fix scroll lock layout shift

* changelog entry

* changelog entry

* prettier

* add notes about browser support
2024-03-06 09:07:14 -05:00
Cory LaViska 7f87887477 update changelog 2024-02-29 11:33:09 -05:00
Amadej Glasenčnik c88b38f194
Add Slovenian translation (#1893) 2024-02-29 11:32:38 -05:00
Cory LaViska e2bce65c02 update changelog 2024-02-29 11:17:40 -05:00
Susanne Kirchner 9cbb0b8a95
Add missing form-control styles import (#1897) 2024-02-29 11:14:58 -05:00
Cory LaViska 2128e62109
fix required content color; closes #1882 (#1889) 2024-02-23 11:47:41 -05:00
Cory LaViska 12ce0217e5 update changelog 2024-02-23 11:43:38 -05:00
RoyDust 1a2969a74b
fix:fix multi-select tag not changing with size (#1886)
Co-authored-by: xiongwei <xiongwei@sobey.com>
2024-02-23 11:42:49 -05:00
Cory LaViska 033fec9471 update changelog 2024-02-21 13:29:40 -05:00
Cory LaViska 8272619663 Merge branch 'cyantree-issue-1815_fix-submenu-closing' into next 2024-02-21 13:28:09 -05:00
cyantree 298892b10a fix race condition in `submenu-controller` (#1815) 2024-02-21 00:27:32 +01:00
Cory LaViska e1102ba9cf prevent tab group safari twitch; fixes #1839 2024-02-20 14:58:11 -05:00
Cory LaViska b589938443
fixes #1709 (#1879) 2024-02-20 14:18:05 -05:00
Cory LaViska 07b13d489a reorder 2024-02-20 14:07:28 -05:00
Cory LaViska e9405d33a8
Fix close behavior when select is in a shadow root; fixes #1859 (#1878)
* fix close behavior when select is in a shadow root

* add pr
2024-02-20 13:55:50 -05:00
Cory LaViska f2a42565e2 prettier 2024-02-20 13:48:47 -05:00
Cory LaViska 23f09dfa79 update changelog 2024-02-20 13:46:09 -05:00
stefanholzapfel 6e288a80a3
feat(sl-popup): Add contextElement property to VirtualElement interface (#1874) 2024-02-20 13:44:53 -05:00
Cory LaViska 9b19c4c782 update changelog 2024-02-20 12:51:59 -05:00
cyantree 6440387432
fix `sl-rating` sometimes not resetting correctly when using `precision` and leaving with the mouse (#1877)
Co-authored-by: cyantree <cyantree@users.noreply.github.com>
2024-02-20 12:49:53 -05:00
Cory LaViska f3be76840f 2.14.0 2024-02-15 10:00:04 -05:00
Cory LaViska 1056a10f8e update version 2024-02-15 09:59:53 -05:00
Cory LaViska f9a73567f7 update lock file 2024-02-15 09:56:24 -05:00
Burton Smith 6bc06d5d95
update install event from `postinstall` to `prepare` (#1868) 2024-02-12 12:29:06 -05:00
Cory LaViska 7571f8c534 remove styles from template 2024-02-09 10:27:09 -05:00
Cory LaViska 1bf3e5a2b7 add missing import to template 2024-02-09 10:20:18 -05:00
Cory LaViska 02ce4dbf4e
Import styles more efficiently (#1861)
* import styles more efficiently; fixes #1692

* remove scale transition
2024-02-09 10:12:47 -05:00
Cory LaViska 775f30107f fix help text a11y 2024-02-09 09:57:54 -05:00
Cory LaViska 9ee1617696 update changelog 2024-02-09 09:33:14 -05:00
Alessandro 7e38e93ab2
fix(carousel): remove check for scrolling (#1862) 2024-02-09 09:28:45 -05:00
Cory LaViska dafb35c6e2 update changelog 2024-02-08 15:20:18 -05:00
Cory LaViska a36bbe2fc4 update changelog 2024-02-08 15:19:51 -05:00
Ahmad Alfy 4185430989
locale: add Arabic translation (#1852) 2024-02-08 15:17:54 -05:00
Cory LaViska e6d3d8317a
Add checkbox help text (#1860)
* add help text to sl-checkbox to match sl-switch

* add missing import
2024-02-08 14:51:00 -05:00
clintcs 9451c3b8de
add switch help text (#1800) 2024-02-08 12:54:21 -05:00
Konnor Rogers a5e9b942e3
fix animated image documentation for CSS part (#1838) 2024-02-08 12:46:31 -05:00
Cory LaViska 380d56fa40
remove html from getTextLabel() (#1840) 2024-02-08 12:42:59 -05:00
Cory LaViska 83fe1ff28e 2.13.1 2024-01-24 11:55:27 -05:00
Cory LaViska a4c49e95a9
no more tomatoes (#1836) 2024-01-24 10:29:45 -05:00
Cory LaViska beea96b373 2.13.0 2024-01-23 11:29:13 -05:00
Cory LaViska 6751b21283 remove unused import 2024-01-23 11:29:05 -05:00
Cory LaViska e37139b7cf remove old test 2024-01-23 11:26:59 -05:00
Cory LaViska 1f87f429ed don't ignore 2024-01-23 11:24:02 -05:00
Cory LaViska afc6dc1923 whitespace 2024-01-23 11:12:47 -05:00
Cory LaViska e2a64486d0 ignore types 2024-01-23 11:12:30 -05:00
Cory LaViska 8473d06822 prettier 2024-01-23 11:10:09 -05:00
Cory LaViska cb15749500 update changelog 2024-01-23 11:08:57 -05:00
Luke Warlow 0a319c3646
Use close watcher when supported in place of escape key handlers (#1788)
* Use close watcher when supported in place of escape key handlers

* Update src/components/select/select.component.ts

---------

Co-authored-by: Cory LaViska <cory@abeautifulsite.net>
2024-01-23 11:07:46 -05:00
Matin 1d626c1357
internals: refactor stop animations resolve mechanism (#1780)
* internals: refactor stop animations resolve mechanism

* remove cancel/finish listeners from stop animations function
2024-01-23 10:48:35 -05:00
YassSSH caf47069c0
Fixing the initial values on doc (#1785)
This commit replaces the string-based 'value' prop with an array in the documentation example related to multiple selection in Shoelace's React components.
2024-01-23 10:45:48 -05:00
Konnor Rogers 773255881b
fix dialog focus trapping behavior (#1813)
* fix dialog focus trapping behavior

* add changelog entry

* prettier

* remove duplicate 'disabled' check in tabbable

* fix dialog stuff

* prettier

* fix logic around checking active elements

* prettier

* prettier

* remove cusrtom-elements.mjs

---------

Co-authored-by: Cory LaViska <cory@abeautifulsite.net>
2024-01-23 10:45:20 -05:00
Cory LaViska 478c8bdf69 update changelog; #1748 2024-01-23 10:42:46 -05:00
Alessandro 9f640aa0a2
fix(carousel): fix issues with safari (#1748)
* fix(carousel): fix scrollend polyfill

* fix(carousel): refactor mouse dragging

* chore: revert original mouse dragging implementation

* fix: add workaround for safari

* chore: add unit tests

* chore: minor changes

* chore: revert change

* chore: skip test case

* chore: revert changes to docs

* chore: remove leftover
2024-01-23 10:39:48 -05:00
Burton Smith b1908d73dc
add vue types (#1797)
* add vue types

* run prettier

* add postinstall script for playwright

* Update docs/pages/frameworks/vue.md

---------

Co-authored-by: Cory LaViska <cory@abeautifulsite.net>
2024-01-23 10:34:36 -05:00
clintcs 1a77e603f8
Add Radio Group `help-text` slot documentation (#1818) 2024-01-23 10:27:22 -05:00
Cory LaViska ac5e2d2d43 update changelog 2024-01-23 10:19:07 -05:00
Alessandro 95881b8cf8
fix(color-picker): add missing percent signs (#1831) 2024-01-23 10:17:42 -05:00
Cory LaViska eb39610a46
fixes #1779 (#1828) 2024-01-23 10:17:16 -05:00
Cory LaViska e231f8a4a1
fixes #1823 (#1826) 2024-01-23 10:17:01 -05:00
Cory LaViska 6b9e78f05d
fixes #1795 (#1822) 2024-01-23 10:16:38 -05:00
Cory LaViska b79c72725b
fixes #1805 (#1821) 2024-01-23 10:15:58 -05:00
Cory LaViska 92bde9c66b
fixes #1730 (#1820) 2024-01-23 10:15:33 -05:00
Cory LaViska dd483c0a04 fix typo 2023-12-13 12:04:39 -05:00
Cory LaViska f5f4f9ae43 reformat comment 2023-12-13 12:03:01 -05:00
Cory LaViska a21ab1d044 update changelog; #1787 2023-12-13 12:00:37 -05:00
Cory LaViska 75c45a2aa7 update settings 2023-12-13 12:00:23 -05:00
Michael Warren d909f4f73d
fix(spinner): fix spinner animation, prevent spinner resize in flex containers (#1787) 2023-12-13 11:40:51 -05:00
Konnor Rogers 7891dbef93
Add missing extensions (#1770)
* fix(typescript): add missing extension to imports in typescript

This is required for the types to work with the new
`--module-resolution=node16`.

The list of places to fix was obtained by a crude script:

```sh
rg -g'**/*.ts' -g'!**/*.test.ts' ' from\s+.\.' | rg -v '\.js'
```

References #1765

* add missing extensions

* revert tsconfig

* prettier

* fix test files for NodeNext

* prettier

* changelog entry

* prettier

* prettier

* prettier

---------

Co-authored-by: Andrey Lushnikov <aslushnikov@gmail.com>
2023-12-08 12:30:31 -05:00
Konnor Rogers b4ed398240
Account for elements with tabbable controls (#1755)
* account for elements with tabbable controls

* prettier

* add changelog entry

* prettier
2023-12-08 12:10:00 -05:00
Cory LaViska 1710cfb8bc update; #1700 2023-12-06 17:06:18 -05:00
Cory LaViska 0080ff9c60 fix trimPipes 2023-12-06 16:53:03 -05:00
Cory LaViska caae94119c
No more pipes (#1771)
* a little whitespace never hurt nobody

* remove pipes from docs
2023-12-06 16:24:21 -05:00
Cory LaViska 59ef323f38 moar prettier 2023-12-06 16:23:39 -05:00
Cory LaViska e1417b8e1a prettier 2023-12-06 16:21:14 -05:00
Cory LaViska bb20126b17 Merge branch 'next' of https://github.com/shoelace-style/shoelace into next 2023-12-06 16:17:12 -05:00
Ryan 3de99eee0a
Add .d.ts files to theme style.js files (#1767) 2023-12-06 16:16:52 -05:00
Cory LaViska 0d043767ec Merge branch 'menu-item-loading' of github.com:mitchray/shoelace into next 2023-12-06 13:57:49 -05:00
Cory LaViska b7eccb1bff
Make sure `<sl-select>` closes when focusing out (#1764)
* fixes #1763

* fix comment

* 🤷🏻‍♂️

* whatever wtr
2023-12-06 11:58:49 -05:00
Konnor Rogers dd27db5196
Further improve tabbable performance (#1750)
* improve tabbable performance

* improve tabbable performance

* add PR #

* prettier

* change to getSlottedChildrenOutsideRootElement

* prettier
2023-12-01 12:06:16 -05:00
Cory LaViska 3e38da210e remove unused style 2023-12-01 10:29:16 -05:00
Cory LaViska 4864ab808d
Fixes `setRangeText()` in `<sl-input>` and `<sl-textarea>` (#1752)
* fix setSelectionRange(); fixes #1746

* remove comment

* remove console.log
2023-12-01 10:06:48 -05:00
Cory LaViska e2b7327d98
Improve tooltip accessibility (#1749)
* always close on escape, even when not focused; #1734

* use fallbacks instead of defaults

* add words

* add safe trapezoids / hover bridge; fixes #1734

* oh, webkit

* remove unused import

* cleanup just in case
2023-12-01 10:02:46 -05:00
Mitch Ray 1a8403b9b2 Reduce size 2023-11-25 09:51:08 +11:00
Konnor Rogers bfa7c4cda9
Run web test runner with production modules (#1736)
* Run web test runner with production modules

* prettier
2023-11-21 11:19:06 -05:00
Cory LaViska 7fae62b806 prettier 2023-11-20 21:09:07 -05:00
Cory LaViska 15c6733949 temporarily disable FF in Web Test Runner 2023-11-20 21:04:57 -05:00
Cory LaViska 1e57a632d9 fix a11y error 2023-11-20 21:04:46 -05:00
Cory LaViska ffe492c503 revert 2023-11-20 20:38:09 -05:00
Cory LaViska 21e2c7a473 try node 20 2023-11-20 20:31:24 -05:00
Cory LaViska b6c9b64ec0 restore ff tests 2023-11-20 19:59:39 -05:00
Cory LaViska 00435ac682 more ff test skips 2023-11-20 19:47:58 -05:00
Matt Obee 4699f99107
Fix 'controlled' typo (#1735) 2023-11-20 19:45:10 -05:00
Rikard Kling 025da5e59f
Small typo (#1728) 2023-11-20 17:09:09 -05:00
Cory LaViska d99b90dee1 2.12.0 2023-11-20 12:17:51 -05:00
Cory LaViska 66c5e4cba2 skip ff 2023-11-20 12:15:17 -05:00
Cory LaViska d7d9242d58 skip because ff 2023-11-20 12:14:11 -05:00
Cory LaViska 02ad181775 skip because firefox 2023-11-20 12:11:58 -05:00
Cory LaViska 024c6e2e48 update deprecated properties 2023-11-20 11:57:54 -05:00
Cory LaViska 3fdbefa2d4 fix 2023-11-17 14:25:56 -05:00
Cory LaViska 2b45c546e8 update playwright install cmd 2023-11-17 14:17:00 -05:00
Cory LaViska a36ae4e482 update playwright version for webkit 2023-11-17 14:03:33 -05:00
Cory LaViska 3b2eb9bb5c re-enable webkit tests 2023-11-17 14:03:22 -05:00
Cory LaViska 1bf490aed0 temp disable webkit 2023-11-17 13:48:45 -05:00
Cory LaViska 1564df829b update WTR 2023-11-17 13:38:58 -05:00
Cory LaViska facb5504a4 prettier 2023-11-17 10:07:57 -05:00
Cory LaViska ee18f3a449 update changelog 2023-11-17 09:51:21 -05:00
folini96 c3c770b0e0
Add italian translations (#1727)
Co-authored-by: Andrea Folini <andrea.folini@skillbill.it>
2023-11-17 09:50:46 -05:00
Nick Lemmon a1888c628f
removes duplicative style declaration in the skeleton component (#1722) 2023-11-14 13:15:22 -05:00
Cory LaViska 13c3e88384 Merge branch 'next' of https://github.com/shoelace-style/shoelace into next 2023-11-13 15:56:26 -05:00
Cory LaViska e0701fe3fc add two-way binding info back 2023-11-13 15:56:23 -05:00
Konnor Rogers 35c2ad886d
Fix nested dialogs (#1711)
* fix nested dialog focus

* fix nested dialog focus

* fix nested dialog focus

* prettier

* remove index.html

* fix tests

* prettier
2023-11-13 14:13:42 -05:00
Coridyn e786aa86b5
Fix React .d.ts files to import from valid path; fixes #1713 (#1714) 2023-11-13 11:09:11 -05:00
Konnor Rogers 5221419816
Fix form controls entering / leaving a form (#1708)
* fix dynamic form controls

* update comment

* add form.checkValidity()

* prettier
2023-11-07 10:39:57 -05:00
Konnor Rogers f015dc9169
fix form controls to read from property instead of attribute (#1707)
* fix form controls to read from properties and attributes

* update changelog

* prettier

* update changelog

* prettier

* small comment fix
2023-11-07 10:28:01 -05:00
Mitch Ray a2b7816010 Keep text shown 2023-11-04 10:05:09 +11:00
Cory LaViska 2a1f48c332 update changelog 2023-11-03 10:25:59 -04:00
Henry Wilkinson 8ddef1a0bd
Updates copy button with Bootstrap Icons 1.11 (#1702) 2023-11-03 10:25:18 -04:00
Mitch Ray 468b0b9e66 Add loading attribute to menu-item 2023-11-03 08:12:27 +11:00
Cory LaViska 6590dd4004 upgrade jet brains plugin and stop writing to package.json 2023-11-02 08:49:58 -04:00
Konnor Rogers 12a45eb65d
only emit sl-change when you stop dragging (#1689)
* only emit sl-change when you stop dragging

* only emit sl-change when you stop dragging

* prettier

* add changelog entry

* update changelog

* update changelog

* update changelog
2023-10-31 14:09:10 -04:00
Konnor Rogers 5e620a8bb3
fix issues with no translation errors for bundled components (#1696) 2023-10-31 13:33:16 -04:00
Cory LaViska b9fa2a60fe 2.11.2 2023-10-25 13:09:11 -04:00
Cory LaViska b7a4a228d6 update changelog 2023-10-25 13:08:51 -04:00
Alessandro 597a06c97c
fix(carousel): add instance check to isCarouselItem (#1684) 2023-10-25 13:04:31 -04:00
Cory LaViska 1087fe23f7 2.11.1 2023-10-25 11:27:57 -04:00
Cory LaViska 207a660738 update version 2023-10-25 10:34:17 -04:00
Cory LaViska 296a24c74a fix slotted image dimensions 2023-10-23 12:02:08 -04:00
Cory LaViska 265ef71e6d fix cspell and ts 2023-10-23 11:55:28 -04:00
Cory LaViska 224bba2532 remove ts-check 2023-10-23 11:35:24 -04:00
Cory LaViska d07f8e01ad update changelog 2023-10-23 11:00:11 -04:00
Alessandro 58bf05451d
fix: multiple slides per page navigation (#1605)
* fix(carousel): change navigation logic

* chore: update tests

* chore: create polyfill for scrollend

* chore: add unit tests and clean up

* chore: leftover

* chore: minor fix

* chore: avoid initialization for clones

* fix(carousel): update page navigation logic

* chore(carousel): revert change

* chore(carousel): minor changes

* chore: update pagination logic

* fix: enforce slidesPerMove value
2023-10-23 10:57:58 -04:00
floflausch f53309b04a
Update angular.md (#1671)
removed the < brackets

Co-authored-by: Cory LaViska <cory@abeautifulsite.net>
2023-10-23 10:48:10 -04:00
Cory LaViska 49b42c3b90 fix script in docs; closes #1670 2023-10-23 10:18:52 -04:00
Cory LaViska 762d0b0098 2.11.0 2023-10-20 09:10:58 -04:00
Cory LaViska e297633bd7 prettier 2023-10-20 09:10:56 -04:00
Cory LaViska f28ea9b834 update version 2023-10-20 09:07:22 -04:00
Cory LaViska 0272e3dcff prettier 2023-10-20 09:05:15 -04:00
floflausch 8d42e9fd7e
Update angular.md (#1670)
Added more instructions for clearity about what to do
2023-10-20 09:00:27 -04:00
Cory LaViska e5da26fe6d
fix empty react index; closes #1659 (#1663) 2023-10-19 10:30:16 -04:00
Cory LaViska eb96e3db4b fix details example 2023-10-19 09:39:13 -04:00
Cory LaViska b1b54a5a34
Fix placeholder color in sl-select (#1667)
* fix placeholder color in sl-select

* add issue number
2023-10-19 09:33:09 -04:00
Cory LaViska a5404ecab0
don't block escape; fixes #1607 (#1661) 2023-10-18 13:42:37 -04:00
Cory LaViska afe7778f89 update changelog 2023-10-18 13:25:43 -04:00
fountainpen 88f3009cf4
Create hr.ts (#1656) 2023-10-18 13:25:02 -04:00
Cory LaViska 8c9f8e69fc 2.10.0 2023-10-16 13:01:29 -04:00
Cory LaViska 2e2d0349d6 prettier 2023-10-16 13:01:26 -04:00
Cory LaViska 6e9abc0226 update version 2023-10-16 12:58:26 -04:00
Cory LaViska 4b03675116
oh, safari (#1655) 2023-10-16 12:55:20 -04:00
Cory LaViska db66bbe5a1 update comments 2023-10-16 12:46:59 -04:00
Cory LaViska 54923edd22 update default 2023-10-16 12:41:22 -04:00
Cory LaViska 86df7f6053 Merge branch 'next' of https://github.com/shoelace-style/shoelace into next 2023-10-16 12:39:06 -04:00
Cory LaViska 3882eb151d remove unused dep 2023-10-16 12:39:05 -04:00
John F Morton 6ef246c575
Update carousel.md to document the default aspect ratio of 16/9. (#1617) 2023-10-16 12:35:00 -04:00
Cory LaViska db333e1b81 Merge branch 'next' of https://github.com/shoelace-style/shoelace into next 2023-10-16 12:32:16 -04:00
Cory LaViska 5155f02dbf update changelog 2023-10-16 12:32:14 -04:00
Konnor Rogers 2643e4ff9e
Fix `tabbable` performance issues in Chrome / Edge (#1614)
* fix: improve tabbable performance

* add note about composed-offset-position

* update package.json

* prettier

* prettier

* prettier
2023-10-16 12:30:34 -04:00
Cory LaViska ff94ea2e0c use discussions for features 2023-10-16 11:23:36 -04:00
Cory LaViska 39a0fafdc3 remove whitespace 2023-10-13 13:53:12 -04:00
Cory LaViska 28cc38a90b Merge branch 'schilchSICKAG-next' into next 2023-10-13 13:50:40 -04:00
Christian Schilling 28da45c2de Updated @lit-labs/react to @lit/react now as this is stable 2023-10-13 09:46:54 +02:00
Christian Schilling 7041357bf5 Updated @lit-labs/react to @lit/react now as this is stable 2023-10-13 09:38:05 +02:00
Cory LaViska d5ab0fef22 update plop, ora, sinon 2023-10-12 14:24:39 -04:00
Cory LaViska c7b53cff47 update prettier 2023-10-12 13:56:11 -04:00
Cory LaViska 85f91b7785 lit 3, eslint, lint-stages updates 2023-10-12 13:29:15 -04:00
Cory LaViska 620fda6e79 typescript + esbuild updates 2023-10-12 12:47:22 -04:00
Cory LaViska 7bf90b64ed update deps (all but majors) 2023-10-12 12:34:13 -04:00
Konnor Rogers ad9ca8fdb5
Fire `sl-select` when clicking an element inside a menu-item (#1599)
* Fire sl-select when clicking an element inside a menu-item

* changelog + remove unused code

* prettier

* prettier

---------

Co-authored-by: Cory LaViska <cory@abeautifulsite.net>
2023-10-12 12:13:00 -04:00
Cory LaViska 236fbd7109
Add safe triangle for submenu selection (#1600)
* add safe triangle; fixes #1550

* make z-index relative to submenu

* refactor submenu properties
2023-10-12 12:03:41 -04:00
Cory LaViska a697b356ac update changelog 2023-10-12 12:00:05 -04:00
jarviszheng eb6966a6cf
Create zh-cn.ts (#1604) 2023-10-12 11:59:22 -04:00
Burton Smith 79e939e929
add docs for web-types (#1608)
* add docs for web-types

* add missing word
2023-10-12 09:39:48 -04:00
Konnor Rogers 7500cabc58
fix focus trapping to respect the currently focused element (#1583)
* fix focus trapping to respect the currently focused element

* prettier

* remove index.html

* fix activeElements

* prettier

* update changelog

* prettier
2023-10-04 15:10:38 -04:00
Cory LaViska 8748394f54 add PR 2023-10-02 09:06:41 -05:00
Cory LaViska 87d82639f8 update changelog 2023-10-02 09:06:11 -05:00
Cam Skene 566f0e41a4
comma separate exportparts (#1586)
Fixes #1585.
2023-10-02 09:04:08 -05:00
Cory LaViska cf85d6af41 fix localize bug 2023-09-27 13:03:35 -04:00
Cory LaViska 789ba7a13c ignore package.json 2023-09-27 13:02:20 -04:00
Cory LaViska fac6e12b4e update changelog 2023-09-26 09:55:21 -04:00
Cory LaViska d56fbb6197 2.9.0 2023-09-26 09:35:48 -04:00
Cory LaViska 1f2407d673 update version 2023-09-26 09:31:12 -04:00
Cory LaViska ed7949261e update changelog 2023-09-26 09:06:48 -04:00
Cory LaViska c9f810ac3e rename private var; #1572 2023-09-26 09:05:17 -04:00
Cory LaViska c900c2a9ca Merge branch 'yringler-only-use-library-for-library' into next 2023-09-26 09:03:09 -04:00
Cory LaViska b7107ace1b Merge branch 'only-use-library-for-library' of github.com:yringler/shoelace into yringler-only-use-library-for-library 2023-09-26 09:02:24 -04:00
Cory LaViska cbd4336773
add support for external modals; fixes #1571 (#1575) 2023-09-26 08:50:11 -04:00
Cory LaViska 9b969339a1 fixes #1576 2023-09-25 09:08:04 -04:00
Cory LaViska 24f7b190f7 fix words; #1578 2023-09-25 09:01:24 -04:00
mfocqueteau a41e4e8928
Fixed typo (Alert doc): "take affect" -> "take effect" (#1578)
One of the examples given in the documentation for Alerts says "Settings will take **affect** on next login". Which is a typo since affect is a verb, not a noun.
2023-09-25 08:59:51 -04:00
Yehuda Ringler 25dd15b92c ONLY-USE: Fix bug: svg url treated as sprite 2023-09-20 16:43:26 -04:00
Cory LaViska 2ed5a4ff97 Merge branch 'next' of https://github.com/shoelace-style/shoelace into next 2023-09-14 12:08:07 -04:00
Cory LaViska 4d3297937a fixes #1548 2023-09-14 12:08:05 -04:00
Christian Schuller 1d28e1bbc5
fix: add missing super.disconnectCallback() calls (#1564) 2023-09-14 11:17:12 -04:00
Alan Chambers 3b77c3b99f
updated react wrapper (#1565)
- updated @lit-labs/react to latest version with improved type compatibility
- added small preact section to the react docs with link to preact/compat typescript config guide
2023-09-14 11:13:29 -04:00
Cory LaViska c858bc3723 update changelog 2023-09-13 11:51:50 -04:00
Wes 317d567fe8
fix(autoloader): only attempt to register root element if it's shoelace element (#1563) 2023-09-13 11:50:02 -04:00
Cory LaViska e6db8c953a update bootstrap icons 2023-09-12 12:09:29 -04:00
Cory LaViska aa2cf24be5 Merge branch 'next' of https://github.com/shoelace-style/shoelace into next 2023-09-08 08:33:42 -04:00
Cory LaViska 42f881806b remove webtypes 2023-09-08 08:32:48 -04:00
Mario Hamann 2b5e8286df
fix: make German translation more consistent + neutral (#1558)
In German you can say "Du" (=informal) or "Sie" (= formal). Before this commit both versions were used at the same time. It is preferred to make interfaces neutral, as some systems use "Du" (iOS, macOS) and some "Sie" (MS, Android). In addition all other translations were neutral, too, so this makes it more consistent.
2023-09-08 08:31:57 -04:00
Cory LaViska 32342f803c Merge branch 'break-stuff-fix-web-types-reference' into next 2023-09-08 08:28:14 -04:00
Burton Smith 7d6f770cd9 simplify implementation 2023-09-07 23:04:31 -04:00
Burton Smith 8d86f374f9 clean up messaging 2023-09-07 21:38:16 -04:00
Burton Smith 242e8e92ae fix web-types reference 2023-09-07 21:32:45 -04:00
Konnor Rogers 883cb161ec
show errors in dev server (#1547)
* show errors in dev server

* fix build

* prettier
2023-08-30 09:42:34 -04:00
Cory LaViska a2fbe121c3
update ctrl/tinycolor; fixes #1542 (#1545) 2023-08-28 09:39:16 -04:00
Cory LaViska ab770c566e
fix spacing; #1540 (#1544) 2023-08-28 09:27:57 -04:00
Konnor Rogers 1867603225
log stderr in builds (#1543) 2023-08-25 16:20:19 -04:00
Cory LaViska cf195da424 fix stuck search 2023-08-25 09:35:05 -04:00
Cory LaViska 0cb6aa5d12 reformat by CEM plugin 2023-08-23 15:36:19 -04:00
227 zmienionych plików z 13432 dodań i 8758 usunięć

Wyświetl plik

@ -1,4 +1,7 @@
contact_links:
- name: Feature Requests
url: https://github.com/shoelace-style/shoelace/discussions/categories/ideas
about: All requests for new features should go here.
- name: Help & Support
url: https://github.com/shoelace-style/shoelace/discussions/categories/help
about: Please don't create issues for personal help requests. Instead, ask your question on the discussion forum.

Wyświetl plik

@ -1,15 +0,0 @@
---
name: Feature Request
about: Suggest an idea for this project.
title: ''
labels: feature
---
### What issue are you having?
Provide a clear and concise description of the problem you're facing.
### Describe the solution you'd like
How would you like to see the library solve it?
### Describe alternatives you've considered
In what ways have you tried to solve this with the current version?

Wyświetl plik

@ -25,6 +25,6 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npx playwright install-deps
- run: npx playwright install --with-deps
- run: npm ci
- run: npm run verify

3
.gitignore vendored
Wyświetl plik

@ -1,9 +1,8 @@
_site
.cache
.DS_Store
cdn
dist
docs/assets/images/sprite.svg
node_modules
src/react
cdn
web-types.json

Wyświetl plik

@ -2,6 +2,6 @@
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
}
}

Wyświetl plik

@ -77,16 +77,6 @@ Shoelace is an open source project and contributions are encouraged! If you're i
## License
Shoelace is designed in New Hampshire by [Cory LaViska](https://twitter.com/claviska). Its available under the terms of the MIT license.
Designing, developing, and supporting this library requires a lot of time, effort, and skill. Id like to keep it open source so everyone can use it, but that doesnt provide me with any income.
**Therefore, if youre 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.
👇 Your support is very much appreciated! 👇
- [Become a sponsor](https://github.com/sponsors/claviska)
- [Star on GitHub](https://github.com/shoelace-style/shoelace/stargazers)
- [Follow on Twitter](https://twitter.com/shoelace_style)
Shoelace was created by [Cory LaViska](https://twitter.com/claviska) and is available under the terms of the MIT license.
Whether you're building Shoelace or building something _with_ Shoelace — have fun creating! 🥾

Wyświetl plik

@ -100,6 +100,7 @@
"monospace",
"mousedown",
"mousemove",
"mouseout",
"mouseup",
"multiselectable",
"nextjs",
@ -109,6 +110,7 @@
"novalidate",
"npmdir",
"Numberish",
"onscrollend",
"outdir",
"ParamagicDev",
"peta",

Wyświetl plik

@ -1,6 +1,7 @@
import * as path from 'path';
import { customElementJetBrainsPlugin } from 'custom-element-jet-brains-integration';
import { customElementVsCodePlugin } from 'custom-element-vs-code-integration';
import { customElementVuejsPlugin } from 'custom-element-vuejs-integration';
import { parse } from 'comment-parser';
import { pascalCase } from 'pascal-case';
import commandLineArgs from 'command-line-args';
@ -38,6 +39,7 @@ export default {
customElementsManifest.package = { name, description, version, author, homepage, license };
}
},
// Infer tag names because we no longer use @customElement decorators.
{
name: 'shoelace-infer-tag-names',
@ -66,6 +68,7 @@ export default {
}
}
},
// Parse custom jsDoc tags
{
name: 'shoelace-custom-tags',
@ -137,6 +140,7 @@ export default {
}
}
},
{
name: 'shoelace-react-event-names',
analyzePhase({ ts, node, moduleDoc }) {
@ -155,6 +159,7 @@ export default {
}
}
},
{
name: 'shoelace-translate-module-paths',
packageLinkPhase({ customElementsManifest }) {
@ -191,6 +196,7 @@ export default {
});
}
},
// Generate custom VS Code data
customElementVsCodePlugin({
outdir,
@ -202,14 +208,23 @@ export default {
}
]
}),
customElementJetBrainsPlugin({
outdir: './dist',
excludeCss: true,
packageJson: false,
referencesTemplate: (_, tag) => {
return {
name: 'Documentation',
url: `https://shoelace.style/components/${tag.replace('sl-', '')}`
};
}
}),
customElementVuejsPlugin({
outdir: './dist/types/vue',
fileName: 'index.d.ts',
componentTypePath: (_, tag) => `../../components/${tag.replace('sl-', '')}/${tag.replace('sl-', '')}.component.js`
})
]
};

Wyświetl plik

@ -160,7 +160,7 @@
</td>
<td>
{% if prop.type.text %}
<code>{{ prop.type.text | markdownInline | safe }}</code>
<code>{{ prop.type.text | trimPipes | markdownInline | safe }}</code>
{% else %}
-
{% endif %}
@ -211,7 +211,7 @@
<td>{{ event.description | markdownInline | safe }}</td>
<td>
{% if event.type.text %}
<code>{{ event.type.text }}</code>
<code>{{ event.type.text | trimPipes }}</code>
{% else %}
-
{% endif %}
@ -245,7 +245,7 @@
{% if method.parameters.length %}
<code>
{% for param in method.parameters %}
{{ param.name }}: {{ param.type.text }}{% if not loop.last %},{% endif %}
{{ param.name }}: {{ param.type.text | trimPipes }}{% if not loop.last %},{% endif %}
{% endfor %}
</code>
{% else %}

Wyświetl plik

@ -95,6 +95,23 @@
</sl-dropdown>
</div>
<a
class="ks-banner{% if toc %} with-toc{% endif %}"
href="https://www.kickstarter.com/projects/fontawesome/web-awesome?ref=71ihfk"
target="_blank"
>
<span>
<svg viewBox="0 0 20 16" xmlns="http://www.w3.org/2000/svg">
<path fill="#f36944" d="M11.63 1.625C11.63 2.27911 11.2435 2.84296 10.6865 3.10064L14 6L17.2622 5.34755C17.0968 5.10642 17 4.81452 17 4.5C17 3.67157 17.6716 3 18.5 3C19.3284 3 20 3.67157 20 4.5C20 5.31157 19.3555 5.9726 18.5504 5.99917L15.0307 13.8207C14.7077 14.5384 13.9939 15 13.2068 15H6.79317C6.00615 15 5.29229 14.5384 4.96933 13.8207L1.44963 5.99917C0.64452 5.9726 0 5.31157 0 4.5C0 3.67157 0.671573 3 1.5 3C2.32843 3 3 3.67157 3 4.5C3 4.81452 2.9032 5.10642 2.73777 5.34755L6 6L9.31702 3.09761C8.76346 2.83855 8.38 2.27656 8.38 1.625C8.38 0.727537 9.10754 0 10.005 0C10.9025 0 11.63 0.727537 11.63 1.625Z"/>
</svg>
<span>
<strong style="white-space: nowrap;">Get ready for more awesome!</strong>
Web Awesome, the next iteration of Shoelace, is on Kickstarter.
</span>
</span>
<span class="faux-button">Read Our Story</span>
</a>
<aside id="sidebar" data-preserve-scroll>
<header>
<a href="/">

Wyświetl plik

@ -9,11 +9,14 @@
*/
/**
* @param {Document} content
* @param {String} rawContent
* @param {Replacements} replacements
*/
module.exports = function (content, replacements) {
module.exports = function (rawContent, replacements) {
let content = rawContent;
replacements.forEach(replacement => {
content.body.innerHTML = content.body.innerHTML.replaceAll(replacement.pattern, replacement.replacement);
content = content.replaceAll(replacement.pattern, replacement.replacement);
});
return content;
};

Wyświetl plik

@ -373,4 +373,12 @@
hide();
}
});
// We're using Turbo, so when a user searches for something, visits a result, and presses the back button, the search
// UI will still be visible but not interactive. This removes the search UI when Turbo renders a page so they don't
// get trapped.
window.addEventListener('turbo:render', () => {
document.body.classList.remove('search-visible');
document.querySelectorAll('.search__overlay, .search__dialog').forEach(el => el.remove());
});
})();

Wyświetl plik

@ -235,7 +235,9 @@ code {
kbd {
background: var(--sl-color-neutral-100);
border: solid 1px var(--sl-color-neutral-200);
box-shadow: inset 0 1px 0 0 var(--sl-color-neutral-0), inset 0 -1px 0 0 var(--sl-color-neutral-200);
box-shadow:
inset 0 1px 0 0 var(--sl-color-neutral-0),
inset 0 -1px 0 0 var(--sl-color-neutral-200);
font-family: var(--sl-font-mono);
font-size: 0.9125em;
border-radius: var(--docs-border-radius);
@ -511,7 +513,9 @@ pre .token.italic {
right: 0;
white-space: normal;
color: var(--sl-color-neutral-800);
transition: 150ms opacity, 150ms scale;
transition:
150ms opacity,
150ms scale;
}
.copy-code-button::part(button) {
@ -659,6 +663,7 @@ pre:hover .copy-code-button,
margin: -1rem;
cursor: pointer;
user-select: none;
-webkit-user-select: none;
}
.content details summary span {
@ -982,7 +987,9 @@ main {
padding: 0.5rem;
margin: 0;
cursor: pointer;
transition: 250ms scale ease, 250ms rotate ease;
transition:
250ms scale ease,
250ms rotate ease;
}
@media screen and (max-width: 900px) {
@ -1052,7 +1059,6 @@ html.sidebar-open #menu-toggle {
padding: 0.5rem;
margin: 0;
cursor: pointer;
transition: 250ms scale ease;
}
#theme-selector:not(:defined) {
@ -1095,12 +1101,6 @@ html.sidebar-open #menu-toggle {
color: var(--sl-color-neutral-1000);
}
#icon-toolbar button:hover,
#icon-toolbar a:hover,
#theme-selector sl-button:hover {
scale: 1.1;
}
#icon-toolbar a:not(:last-child),
#icon-toolbar button:not(:last-child) {
margin-right: 0.25rem;
@ -1413,3 +1413,95 @@ body[data-page^='/tokens/'] .table-wrapper td:first-child code {
grid-column-start: span 6;
}
}
.ks-banner {
display: flex;
gap: 1rem;
position: absolute;
top: 1rem;
width: 950px;
left: calc(50% - 475px);
font-size: 0.9375rem;
align-items: center;
justify-content: space-between;
background: #1a3256;
border-radius: var(--sl-border-radius-large);
padding: 1rem 1.25rem;
color: #fdfdfd;
text-decoration: none;
line-height: 1.4;
z-index: 2;
margin-left: 160px;
}
.ks-banner:hover {
color: #fdfdfd;
}
.ks-banner > span {
display: flex;
align-items: center;
gap: 1rem;
}
.ks-banner svg {
flex: 0 0 1.5rem;
width: 1.5rem;
height: 1.5rem;
}
.ks-banner .faux-button {
display: inline-flex;
align-items: center;
height: 30px;
background: white;
border: solid 1px #d4d4d4;
border-radius: var(--sl-border-radius-medium);
font-size: 0.8375rem;
color: #353439;
padding: 0.5rem 1rem;
white-space: nowrap;
}
.ks-banner.with-toc {
width: 1100px;
left: calc(50% - 550px);
margin-left: 140px;
}
main {
margin-top: 70px;
}
@media screen and (max-width: 1650px) {
.ks-banner,
.ks-banner.with-toc {
width: 540px !important;
top: 50px;
left: calc(50% - 270px);
}
main {
margin-top: 140px;
}
}
@media screen and (max-width: 900px) {
.ks-banner,
.ks-banner.with-toc {
margin-left: 0;
}
}
@media screen and (max-width: 680px) {
.ks-banner,
.ks-banner.with-toc {
width: calc(100% - 2rem) !important;
left: 1rem;
flex-direction: column;
}
main {
margin-top: 150px;
}
}

Wyświetl plik

@ -96,6 +96,12 @@ module.exports = function (eleventyConfig) {
return shoelaceFlavoredMarkdown.renderInline(content);
});
// Trims whitespace and pipes from the start and end of a string. Useful for CEM types, which can be pipe-delimited.
// With Prettier 3, this means a leading pipe will exist if the line wraps.
eleventyConfig.addFilter('trimPipes', content => {
return typeof content === 'string' ? content.replace(/^(\s|\|)/g, '').replace(/(\s|\|)$/g, '') : content;
});
eleventyConfig.addFilter('classNameToComponentName', className => {
let name = capitalCase(className.replace(/^Sl/, ''));
if (name === 'Qr Code') name = 'QR Code'; // manual override
@ -109,7 +115,13 @@ module.exports = function (eleventyConfig) {
//
// Transforms
//
eleventyConfig.addTransform('html-transform', function (content) {
eleventyConfig.addTransform('html-transform', function (rawContent) {
let content = replacer(rawContent, [
{ pattern: '%VERSION%', replacement: customElementsManifest.package.version },
{ pattern: '%CDNDIR%', replacement: cdndir },
{ pattern: '%NPMDIR%', replacement: npmdir }
]);
// Parse the template and get a Document object
const doc = new JSDOM(content, {
// We must set a default URL so links are parsed with a hostname. Let's use a bogus TLD so we can easily
@ -134,11 +146,6 @@ module.exports = function (eleventyConfig) {
scrollingTables(doc);
copyCodeButtons(doc); // must be after codePreviews + highlightCodeBlocks
typography(doc, '#content');
replacer(doc, [
{ pattern: '%VERSION%', replacement: customElementsManifest.package.version },
{ pattern: '%CDNDIR%', replacement: cdndir },
{ pattern: '%NPMDIR%', replacement: npmdir }
]);
// Serialize the Document object to an HTML string and prepend the doctype
content = `<!DOCTYPE html>\n${doc.documentElement.outerHTML}`;

Wyświetl plik

@ -54,7 +54,7 @@ Set the `variant` attribute to change the alert's variant.
<sl-alert variant="neutral" open>
<sl-icon slot="icon" name="gear"></sl-icon>
<strong>Your settings have been updated</strong><br />
Settings will take affect on next login.
Settings will take effect on next login.
</sl-alert>
<br />
@ -102,7 +102,7 @@ const App = () => (
<SlIcon slot="icon" name="gear" />
<strong>Your settings have been updated</strong>
<br />
Settings will take affect on next login.
Settings will take effect on next login.
</SlAlert>
<br />
@ -276,7 +276,7 @@ You should always use the `closable` attribute so users can dismiss the notifica
<sl-alert variant="neutral" duration="3000" closable>
<sl-icon slot="icon" name="gear"></sl-icon>
<strong>Your settings have been updated</strong><br />
Settings will take affect on next login.
Settings will take effect on next login.
</sl-alert>
<sl-alert variant="warning" duration="3000" closable>
@ -361,7 +361,7 @@ const App = () => {
<SlIcon slot="icon" name="gear" />
<strong>Your settings have been updated</strong>
<br />
Settings will take affect on next login.
Settings will take effect on next login.
</SlAlert>
<SlAlert ref={warning} variant="warning" duration="3000" closable>

Wyświetl plik

@ -236,7 +236,7 @@ When a `target` is set, the link will receive `rel="noreferrer noopener"` for [s
### Setting a Custom Width
As expected, buttons can be given a custom width by setting the `width` attribute. This is useful for making buttons span the full width of their container on smaller screens.
As expected, buttons can be given a custom width by passing inline styles to the component (or using a class). This is useful for making buttons span the full width of their container on smaller screens.
```html:preview
<sl-button variant="default" size="small" style="width: 100%; margin-bottom: 1rem;">Small</sl-button>
@ -417,7 +417,7 @@ const App = () => (
### Loading
Use the `loading` attribute to make a button busy. The width will remain the same as before, preventing adjacent elements from moving around. Clicks will be suppressed until the loading state is removed.
Use the `loading` attribute to make a button busy. The width will remain the same as before, preventing adjacent elements from moving around.
```html:preview
<sl-button variant="default" loading>Default</sl-button>

Wyświetl plik

@ -803,7 +803,7 @@ const App = () => (
### Aspect Ratio
Use the `--aspect-ratio` custom property to customize the size of the carousel's viewport.
Use the `--aspect-ratio` custom property to customize the size of the carousel's viewport from the default value of 16/9.
```html:preview
<sl-carousel class="aspect-ratio" navigation pagination style="--aspect-ratio: 3/2;">
@ -1246,7 +1246,7 @@ const App = () => {
<img
alt={`Thumbnail by ${i + 1}`}
className={`thumbnails__image ${i === currentSlide ? 'active' : ''}`}
onCLick={() => handleThumbnailClick(i)}
onClick={() => handleThumbnailClick(i)}
src={src}
/>
)}

Wyświetl plik

@ -89,6 +89,20 @@ const App = () => (
);
```
### Help Text
Add descriptive help text to a switch with the `help-text` attribute. For help texts that contain HTML, use the `help-text` slot instead.
```html:preview
<sl-checkbox help-text="What should the user know about the checkbox?">Label</sl-checkbox>
```
```jsx:react
import SlCheckbox from '@shoelace-style/shoelace/dist/react/checkbox';
const App = () => <SlCheckbox help-text="What should the user know about the switch?">Label</SlCheckbox>;
```
### Custom Validity
Use the `setCustomValidity()` method to set a custom validation message. This will prevent the form from submitting and make the browser display the error message you provide. To clear the error, call this function with an empty string.

Wyświetl plik

@ -123,7 +123,9 @@ Details are designed to function independently, but you can simulate a group or
// Close all other details when one is shown
container.addEventListener('sl-show', event => {
[...container.querySelectorAll('sl-details')].map(details => (details.open = event.target === details));
if (event.target.localName === 'sl-details') {
[...container.querySelectorAll('sl-details')].map(details => (details.open = event.target === details));
}
});
</script>

Wyświetl plik

@ -60,35 +60,6 @@ const App = () => (
## Examples
### Disabled
Add the `disabled` attribute to disable the menu item so it cannot be selected.
```html:preview
<sl-menu style="max-width: 200px;">
<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>
```
{% raw %}
```jsx:react
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
const App = () => (
<SlMenu style={{ maxWidth: '200px' }}>
<SlMenuItem>Option 1</SlMenuItem>
<SlMenuItem disabled>Option 2</SlMenuItem>
<SlMenuItem>Option 3</SlMenuItem>
</SlMenu>
);
```
{% endraw %}
### Prefix & Suffix
Add content to the start and end of menu items using the `prefix` and `suffix` slots.
@ -151,6 +122,64 @@ const App = () => (
{% endraw %}
### Disabled
Add the `disabled` attribute to disable the menu item so it cannot be selected.
```html:preview
<sl-menu style="max-width: 200px;">
<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>
```
{% raw %}
```jsx:react
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
const App = () => (
<SlMenu style={{ maxWidth: '200px' }}>
<SlMenuItem>Option 1</SlMenuItem>
<SlMenuItem disabled>Option 2</SlMenuItem>
<SlMenuItem>Option 3</SlMenuItem>
</SlMenu>
);
```
{% endraw %}
### Loading
Use the `loading` attribute to indicate that a menu item is busy. Like a disabled menu item, clicks will be suppressed until the loading state is removed.
```html:preview
<sl-menu style="max-width: 200px;">
<sl-menu-item>Option 1</sl-menu-item>
<sl-menu-item loading>Option 2</sl-menu-item>
<sl-menu-item>Option 3</sl-menu-item>
</sl-menu>
```
{% raw %}
```jsx:react
import SlMenu from '@shoelace-style/shoelace/dist/react/menu';
import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item';
const App = () => (
<SlMenu style={{ maxWidth: '200px' }}>
<SlMenuItem>Option 1</SlMenuItem>
<SlMenuItem loading>Option 2</SlMenuItem>
<SlMenuItem>Option 3</SlMenuItem>
</SlMenu>
);
```
{% endraw %}
### Checkbox Menu Items
Set the `type` attribute to `checkbox` to create a menu item that will toggle on and off when selected. You can use the `checked` attribute to set the initial state.

Wyświetl plik

@ -1530,6 +1530,140 @@ const App = () => {
};
```
### Hover Bridge
When a gap exists between the anchor and the popup element, this option will add a "hover bridge" that fills the gap using an invisible element. This makes listening for events such as `mouseover` and `mouseout` more sane because the pointer never technically leaves the element. The hover bridge will only be drawn when the popover is active. For demonstration purposes, the bridge in this example is shown in orange.
```html:preview
<div class="popup-hover-bridge">
<sl-popup placement="top" hover-bridge distance="10" skidding="0" active>
<span slot="anchor"></span>
<div class="box"></div>
</sl-popup>
<br>
<sl-switch checked>Hover Bridge</sl-switch><br>
<sl-range min="0" max="50" step="1" value="10" label="Distance"></sl-range>
<sl-range min="-50" max="50" step="1" value="0" label="Skidding"></sl-range>
</div>
<style>
.popup-hover-bridge span[slot='anchor'] {
display: inline-block;
width: 150px;
height: 150px;
border: dashed 2px var(--sl-color-neutral-600);
margin: 50px;
}
.popup-hover-bridge .box {
width: 100px;
height: 50px;
background: var(--sl-color-primary-600);
border-radius: var(--sl-border-radius-medium);
}
.popup-hover-bridge sl-range {
max-width: 260px;
margin-top: .5rem;
}
.popup-hover-bridge sl-popup::part(hover-bridge) {
background: tomato;
opacity: .5;
}
</style>
<script>
const container = document.querySelector('.popup-hover-bridge');
const popup = container.querySelector('sl-popup');
const hoverBridge = container.querySelector('sl-switch');
const distance = container.querySelector('sl-range[label="Distance"]');
const skidding = container.querySelector('sl-range[label="Skidding"]');
distance.addEventListener('sl-input', () => (popup.distance = distance.value));
skidding.addEventListener('sl-input', () => (popup.skidding = skidding.value));
hoverBridge.addEventListener('sl-change', () => (popup.hoverBridge = hoverBridge.checked));
</script>
```
```jsx:react
import { useState } from 'react';
import SlPopup from '@shoelace-style/shoelace/dist/react/popup';
import SlRange from '@shoelace-style/shoelace/dist/react/range';
import SlSwitch from '@shoelace-style/shoelace/dist/react/switch';
const css = `
.popup-hover-bridge span[slot='anchor'] {
display: inline-block;
width: 150px;
height: 150px;
border: dashed 2px var(--sl-color-neutral-600);
margin: 50px;
}
.popup-hover-bridge .box {
width: 100px;
height: 50px;
background: var(--sl-color-primary-600);
border-radius: var(--sl-border-radius-medium);
}
.popup-hover-bridge sl-range {
max-width: 260px;
margin-top: .5rem;
}
.popup-hover-bridge sl-popup::part(hover-bridge) {
background: tomato;
opacity: .5;
}
`;
const App = () => {
const [hoverBridge, setHoverBridge] = useState(true);
const [distance, setDistance] = useState(10);
const [skidding, setSkidding] = useState(0);
return (
<>
<div class="popup-hover-bridge">
<SlPopup placement="top" hover-bridge={hoverBridge} distance={distance} skidding={skidding} active>
<span slot="anchor" />
<div class="box" />
</SlPopup>
<br />
<SlSwitch
checked={hoverBridge}
onSlChange={event => setHoverBridge(event.target.checked)}
>
Hover Bridge
</SlSwitch><br />
<SlRange
min="0"
max="50"
step="1"
value={distance}
label="Distance"
onSlInput={event => setDistance(event.target.value)}
/>
<SlRange
min="-50"
max="50"
step="1"
value={skidding}
label="Skidding"
onSlInput={event => setSkidding(event.target.value)}
/>
</div>
<style>{css}</style>
</>
);
};
```
### Virtual Elements
In most cases, popups are anchored to an actual element. Sometimes, it can be useful to anchor them to a non-element. To do this, you can pass a `VirtualElement` to the anchor property. A virtual element must contain a function called `getBoundingClientRect()` that returns a [`DOMRect`](https://developer.mozilla.org/en-US/docs/Web/API/DOMRect) object as shown below.
@ -1705,3 +1839,15 @@ const App = () => {
);
};
```
Sometimes the `getBoundingClientRects` might be derived from a real element. In this case provide the anchor element as context to ensure clipping and position updates for the popup work well.
```ts
const virtualElement = {
getBoundingClientRect() {
// ...
return { width, height, x, y, top, left, right, bottom };
},
contextElement: anchorElement
};
```

Wyświetl plik

@ -87,26 +87,26 @@ const App = () => (
Use the `size` attribute to change a radio button's size.
```html:preview
<sl-radio-group label="Select an option" name="a" value="1">
<sl-radio-button size="small" value="1">Option 1</sl-radio-button>
<sl-radio-button size="small" value="2">Option 2</sl-radio-button>
<sl-radio-button size="small" value="3">Option 3</sl-radio-button>
<sl-radio-group size="small" label="Select an option" name="a" value="1">
<sl-radio-button value="1">Option 1</sl-radio-button>
<sl-radio-button value="2">Option 2</sl-radio-button>
<sl-radio-button value="3">Option 3</sl-radio-button>
</sl-radio-group>
<br />
<sl-radio-group label="Select an option" name="a" value="1">
<sl-radio-button size="medium" value="1">Option 1</sl-radio-button>
<sl-radio-button size="medium" value="2">Option 2</sl-radio-button>
<sl-radio-button size="medium" value="3">Option 3</sl-radio-button>
<sl-radio-group size="medium" label="Select an option" name="a" value="1">
<sl-radio-button value="1">Option 1</sl-radio-button>
<sl-radio-button value="2">Option 2</sl-radio-button>
<sl-radio-button value="3">Option 3</sl-radio-button>
</sl-radio-group>
<br />
<sl-radio-group label="Select an option" name="a" value="1">
<sl-radio-button size="large" value="1">Option 1</sl-radio-button>
<sl-radio-button size="large" value="2">Option 2</sl-radio-button>
<sl-radio-button size="large" value="3">Option 3</sl-radio-button>
<sl-radio-group size="large" label="Select an option" name="a" value="1">
<sl-radio-button value="1">Option 1</sl-radio-button>
<sl-radio-button value="2">Option 2</sl-radio-button>
<sl-radio-button value="3">Option 3</sl-radio-button>
</sl-radio-group>
```
@ -115,26 +115,26 @@ import SlRadioButton from '@shoelace-style/shoelace/dist/react/radio-button';
import SlRadioGroup from '@shoelace-style/shoelace/dist/react/radio-group';
const App = () => (
<SlRadioGroup label="Select an option" name="a" value="1">
<SlRadioButton size="small" value="1">Option 1</SlRadioButton>
<SlRadioButton size="small" value="2">Option 2</SlRadioButton>
<SlRadioButton size="small" value="3">Option 3</SlRadioButton>
<SlRadioGroup size="small" label="Select an option" name="a" value="1">
<SlRadioButton value="1">Option 1</SlRadioButton>
<SlRadioButton value="2">Option 2</SlRadioButton>
<SlRadioButton value="3">Option 3</SlRadioButton>
</SlRadioGroup>
<br />
<SlRadioGroup label="Select an option" name="a" value="1">
<SlRadioButton size="medium" value="1">Option 1</SlRadioButton>
<SlRadioButton size="medium" value="2">Option 2</SlRadioButton>
<SlRadioButton size="medium" value="3">Option 3</SlRadioButton>
<SlRadioGroup size="medium" label="Select an option" name="a" value="1">
<SlRadioButton value="1">Option 1</SlRadioButton>
<SlRadioButton value="2">Option 2</SlRadioButton>
<SlRadioButton value="3">Option 3</SlRadioButton>
</SlRadioGroup>
<br />
<SlRadioGroup label="Select an option" name="a" value="1">
<SlRadioButton size="large" value="1">Option 1</SlRadioButton>
<SlRadioButton size="large" value="2">Option 2</SlRadioButton>
<SlRadioButton size="large" value="3">Option 3</SlRadioButton>
<SlRadioGroup size="large" label="Select an option" name="a" value="1">
<SlRadioButton value="1">Option 1</SlRadioButton>
<SlRadioButton value="2">Option 2</SlRadioButton>
<SlRadioButton value="3">Option 3</SlRadioButton>
</SlRadioGroup>
);
```
@ -144,26 +144,26 @@ const App = () => (
Use the `pill` attribute to give radio buttons rounded edges.
```html:preview
<sl-radio-group label="Select an option" name="a" value="1">
<sl-radio-button pill size="small" value="1">Option 1</sl-radio-button>
<sl-radio-button pill size="small" value="2">Option 2</sl-radio-button>
<sl-radio-button pill size="small" value="3">Option 3</sl-radio-button>
<sl-radio-group size="small" label="Select an option" name="a" value="1">
<sl-radio-button pill value="1">Option 1</sl-radio-button>
<sl-radio-button pill value="2">Option 2</sl-radio-button>
<sl-radio-button pill value="3">Option 3</sl-radio-button>
</sl-radio-group>
<br />
<sl-radio-group label="Select an option" name="a" value="1">
<sl-radio-button pill size="medium" value="1">Option 1</sl-radio-button>
<sl-radio-button pill size="medium" value="2">Option 2</sl-radio-button>
<sl-radio-button pill size="medium" value="3">Option 3</sl-radio-button>
<sl-radio-group size="medium" label="Select an option" name="a" value="1">
<sl-radio-button pill value="1">Option 1</sl-radio-button>
<sl-radio-button pill value="2">Option 2</sl-radio-button>
<sl-radio-button pill value="3">Option 3</sl-radio-button>
</sl-radio-group>
<br />
<sl-radio-group label="Select an option" name="a" value="1">
<sl-radio-button pill size="large" value="1">Option 1</sl-radio-button>
<sl-radio-button pill size="large" value="2">Option 2</sl-radio-button>
<sl-radio-button pill size="large" value="3">Option 3</sl-radio-button>
<sl-radio-group size="large" label="Select an option" name="a" value="1">
<sl-radio-button pill value="1">Option 1</sl-radio-button>
<sl-radio-button pill value="2">Option 2</sl-radio-button>
<sl-radio-button pill value="3">Option 3</sl-radio-button>
</sl-radio-group>
```
@ -172,26 +172,26 @@ import SlRadioButton from '@shoelace-style/shoelace/dist/react/radio-button';
import SlRadioGroup from '@shoelace-style/shoelace/dist/react/radio-group';
const App = () => (
<SlRadioGroup label="Select an option" name="a" value="1">
<SlRadioButton pill size="small" value="1">Option 1</SlRadioButton>
<SlRadioButton pill size="small" value="2">Option 2</SlRadioButton>
<SlRadioButton pill size="small" value="3">Option 3</SlRadioButton>
<SlRadioGroup size="small" label="Select an option" name="a" value="1">
<SlRadioButton pill value="1">Option 1</SlRadioButton>
<SlRadioButton pill value="2">Option 2</SlRadioButton>
<SlRadioButton pill value="3">Option 3</SlRadioButton>
</SlRadioGroup>
<br />
<SlRadioGroup label="Select an option" name="a" value="1">
<SlRadioButton pill size="medium" value="1">Option 1</SlRadioButton>
<SlRadioButton pill size="medium" value="2">Option 2</SlRadioButton>
<SlRadioButton pill size="medium" value="3">Option 3</SlRadioButton>
<SlRadioGroup size="medium" label="Select an option" name="a" value="1">
<SlRadioButton pill value="1">Option 1</SlRadioButton>
<SlRadioButton pill value="2">Option 2</SlRadioButton>
<SlRadioButton pill value="3">Option 3</SlRadioButton>
</SlRadioGroup>
<br />
<SlRadioGroup label="Select an option" name="a" value="1">
<SlRadioButton pill size="large" value="1">Option 1</SlRadioButton>
<SlRadioButton pill size="large" value="2">Option 2</SlRadioButton>
<SlRadioButton pill size="large" value="3">Option 3</SlRadioButton>
<SlRadioGroup size="large" label="Select an option" name="a" value="1">
<SlRadioButton pill value="1">Option 1</SlRadioButton>
<SlRadioButton pill value="2">Option 2</SlRadioButton>
<SlRadioButton pill value="3">Option 3</SlRadioButton>
</SlRadioGroup>
);
```

Wyświetl plik

@ -233,7 +233,7 @@ import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect label="Select a Few" value="option-1 option-2 option-3" multiple clearable>
<SlSelect label="Select a Few" value={["option-1", "option-2", "option-3"]} multiple clearable>
<SlOption value="option-1">Option 1</SlOption>
<SlOption value="option-2">Option 2</SlOption>
<SlOption value="option-3">Option 3</SlOption>
@ -269,7 +269,7 @@ import SlOption from '@shoelace-style/shoelace/dist/react/option';
import SlSelect from '@shoelace-style/shoelace/dist/react/select';
const App = () => (
<SlSelect value="option-1 option-2" multiple clearable>
<SlSelect value={["option-1", "option-2"]} multiple clearable>
<SlOption value="option-1">Option 1</SlOption>
<SlOption value="option-2">Option 2</SlOption>
<SlOption value="option-3">Option 3</SlOption>

Wyświetl plik

@ -75,6 +75,20 @@ const App = () => (
);
```
### Help Text
Add descriptive help text to a switch with the `help-text` attribute. For help texts that contain HTML, use the `help-text` slot instead.
```html:preview
<sl-switch help-text="What should the user know about the switch?">Label</sl-switch>
```
```jsx:react
import SlSwitch from '@shoelace-style/shoelace/dist/react/checkbox';
const App = () => <SlSwitch help-text="What should the user know about the switch?">Label</SlSwitch>;
```
### Custom Styles
Use the available custom properties to change how the switch is styled.

Wyświetl plik

@ -5,26 +5,6 @@ meta:
layout: component
---
```html:preview
<sl-tab>Tab</sl-tab>
<sl-tab active>Active</sl-tab>
<sl-tab closable>Closable</sl-tab>
<sl-tab disabled>Disabled</sl-tab>
```
```jsx:react
import SlTab from '@shoelace-style/shoelace/dist/react/tab';
const App = () => (
<>
<SlTab>Tab</SlTab>
<SlTab active>Active</SlTab>
<SlTab closable>Closable</SlTab>
<SlTab disabled>Disabled</SlTab>
</>
);
```
:::tip
Additional demonstrations can be found in the [tab group examples](/components/tab-group).
:::

Wyświetl plik

@ -249,7 +249,7 @@ const App = () => (
### Manual Trigger
Tooltips can be controller programmatically by setting the `trigger` attribute to `manual`. Use the `open` attribute to control when the tooltip is shown.
Tooltips can be controlled programmatically by setting the `trigger` attribute to `manual`. Use the `open` attribute to control when the tooltip is shown.
```html:preview
<sl-button style="margin-right: 4rem;">Toggle Manually</sl-button>

Wyświetl plik

@ -10,16 +10,41 @@ Angular [plays nice](https://custom-elements-everywhere.com/#angular) with custo
## Installation
### Download the npm package
To add Shoelace to your Angular app, install the package from npm.
```bash
npm install @shoelace-style/shoelace
```
Next, [include a theme](/getting-started/themes) and set the [base path](/getting-started/installation#setting-the-base-path) for icons and other assets. In this example, we'll import the light theme and use the CDN as a base path.
### Update the Angular Configuration
Next, [include a theme](/getting-started/themes). In this example, we'll import the light theme.
Its also important to load the components by using a `<script>` tag into the index.html file. However, the Angular way to do it is by adding a script configurations into your angular.json file as follows:
```json
"architect": {
"build": {
...
"options": {
...
"styles": [
"src/styles.scss",
"@shoelace-style/shoelace/dist/themes/light.css"
],
"scripts": [
"@shoelace-style/shoelace/dist/shoelace.js"
]
...
```
### Setting up the base path
Next, set the [base path](/getting-started/installation#setting-the-base-path) for icons and other assets in the `main.ts`. In this example, we'll use the CDN as a base path.
```jsx
import '@shoelace-style/shoelace/%NPMDIR%/themes/light.css';
import { setBasePath } from '@shoelace-style/shoelace/%NPMDIR%/utilities/base-path';
setBasePath('https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@%VERSION%/%CDNDIR%/');

Wyświetl plik

@ -32,6 +32,10 @@ If you'd rather not use the CDN for assets, you can create a [build task](https:
Now you can start using components!
### Preact
Preact users facing type errors using components may benefit from setting "paths" in their tsconfig.json so that react types will instead resolve to preact/compat as described in [Preact's typescript documentation](https://preactjs.com/guide/v10/typescript/#typescript-preactcompat-configuration).
## Usage
### Importing Components

Wyświetl plik

@ -23,6 +23,7 @@ npm install @shoelace-style/shoelace
Next, [include a theme](/getting-started/themes) and set the [base path](/getting-started/installation#setting-the-base-path) for icons and other assets. In this example, we'll import the light theme and use the CDN as a base path.
```jsx
// main.js or main.ts
import '@shoelace-style/shoelace/dist/themes/light.css';
import { setBasePath } from '@shoelace-style/shoelace/dist/utilities/base-path';
@ -35,35 +36,22 @@ If you'd rather not use the CDN for assets, you can create a build task that cop
## Configuration
You'll need to tell Vue to ignore Shoelace components. This is pretty easy because they all start with `sl-`.
```js
import { fileURLToPath, URL } from 'url';
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
isCustomElement: tag => tag.startsWith('sl-')
}
}
})
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
});
```
If you haven't configured your Vue.js project to work with custom elements/web components, follow [the instructions here](https://vuejs.org/guide/extras/web-components.html#using-custom-elements-in-vue) based on your project type to ensure your project will not throw an error when it encounters a custom element.
Now you can start using Shoelace components in your app!
## Types
Once you have configured your application for custom elements, you should be able to use Shoelace in your application without it causing any errors. Unfortunately, this doesn't register the custom elements to behave like components built using Vue. To provide autocomplete information and type safety for your components, you can import the Shoelace Vue types into your `tsconfig.json` to get better integration in your standard Vue and JSX templates.
```json
{
"compilerOptions": {
"types": ["@shoelace-style/shoelace/dist/types/vue"]
}
}
```
## Usage
### QR code generator example
@ -107,13 +95,26 @@ When binding complex data such as objects and arrays, use the `.prop` modifier t
<sl-color-picker :swatches.prop="mySwatches" />
```
### Two-way Binding
One caveat is there's currently [no support for v-model on custom elements](https://github.com/vuejs/vue/issues/7830), but you can still achieve two-way binding manually.
```html
<!-- This doesn't work -->
<sl-input v-model="name"></sl-input>
<!-- This works, but it's a bit longer -->
<sl-input :value="name" @input="name = $event.target.value"></sl-input>
```
If that's too verbose for your liking, you can use a custom directive instead. [This utility](https://www.npmjs.com/package/@shoelace-style/vue-sl-model) adds a custom directive that will work just like `v-model` but for Shoelace components.
:::tip
Are you using Shoelace with Vue? [Help us improve this page!](https://github.com/shoelace-style/shoelace/blob/next/docs/frameworks/vue.md)
:::
### Slots
To use Shoelace components with slots, follow the Vue documentation on using [slots with custom elements](https://vuejs.org/guide/extras/web-components.html#building-custom-elements-with-vue).
Slots in Shoelace/web components are functionally the same as basic slots in Vue. Slots can be assigned to elements using the `slot` attribute followed by the name of the slot it is being assigned to.
Here is an example:

Wyświetl plik

@ -208,13 +208,34 @@ Shoelace ships with a file called `vscode.html-custom-data.json` that can be use
}
```
If `settings.json` already exists, simply add the above line to the root of the object. Note that you may need to restart VS Code for the changes to take affect.
If `settings.json` already exists, simply add the above line to the root of the object. Note that you may need to restart VS Code for the changes to take effect.
## JetBrains IDEs
### JetBrains IDEs
If you are using a [JetBrains IDE](https://www.jetbrains.com/) and you are installing Shoelace from NPM, the editor will automatically detect the `web-types.json` file from the package and you should immediately see component information in your editor.
If you are installing from the CDN, you can [download a local copy](https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace/cdn/web-types.json) and add it to the root of your project.
If you are installing from the CDN, you can [download a local copy](https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace/dist/web-types.json) and add it to the root of your project. Be sure to add a reference to the `web-types.json` file in your `package.json` in order for your editor to properly detect it.
```json
{
...
"web-types": "./web-types.json"
...
}
```
If you are using types from multiple projects, you can add an array of references.
```json
{
...
"web-types": [
...,
"./web-types.json"
]
...
}
```
### Other Editors

Wyświetl plik

@ -86,12 +86,10 @@ With Shoelace, you can:
- Incrementally adopt components as needed (no need to ditch your framework)
- Upgrade or switch frameworks without rebuilding foundational components
If your organization is looking to build a design system, [Shoelace will save you thousands of dollars](https://medium.com/eightshapes-llc/and-you-thought-buttons-were-easy-26eb5b5c1871).\* All the foundational components you need are right here, ready to be customized for your brand. And since it's built on web standards, browsers will continue to support it for many years to come.
If your organization is looking to build a design system, [Shoelace will save you thousands of dollars](https://medium.com/eightshapes-llc/and-you-thought-buttons-were-easy-26eb5b5c1871). All the foundational components you need are right here, ready to be customized for your brand. And since it's built on web standards, browsers will continue to support it for many years to come.
Whether you use Shoelace as a starting point for your organization's design system or for a fun personal project, there's no limit to what you can do with it.
<small>\*Please consider giving back some of what you save by [supporting this project with a sponsorship](https://github.com/sponsors/claviska).</small>
## Browser Support
Shoelace is tested in the latest two versions of the following browsers.

Wyświetl plik

@ -12,6 +12,133 @@ Components with the <sl-badge variant="warning" pill>Experimental</sl-badge> bad
New versions of Shoelace are released as-needed and generally occur when a critical mass of changes have accumulated. At any time, you can see what's coming in the next release by visiting [next.shoelace.style](https://next.shoelace.style).
## Next
- `<sl-tab>` `closable` property now reflects. [#2041]
- `<sl-tab-group>` now implements a proper "roving tabindex" and `<sl-tab>` is no longer tabbable by default. This aligns closer to the APG pattern for tabs. [#2041]
- Fixed a bug in the submenu controller that prevented submenus from rendering in RTL without explicitly setting `dir` on the parent menu item [#1992]
## 2.15.1
- Fixed a bug in `<sl-radio-group>` where if a click did not contain a `<sl-radio>` it would show a console error. [#2009]
- Fixed a bug in `<sl-split-panel>` that caused it not to recalculate it's position when going from being `display: none;` to its original display value. [#1942]
- Fixed a bug in `<dialog>` where when it showed it would cause a layout shift. [#1967]
- Fixed a bug in `<sl-tooltip>` that allowed unwanted text properties to leak in [#1947]
- Fixed a bug in `<sl-button-group>` classes [#1974]
- Fixed a bug in `<sl-textarea>` that may throw errors on `disconnectedCallback` in test environments [#1985]
- Fixed a bug in `<sl-color-picker>` that would log a non-passive event listener warning [#2005]
- Fixed a bug in the submenu controller that allowed submenus to go offscreen and not be scrollable [#2001]
- Fixed a bug in `<sl-range>` that caused the tooltip position to be incorrect in some cases [#1979]
## 2.15.0
- Added the Slovenian translation [#1893]
- Added support for `contextElement` to `VirtualElements` in `<sl-popup>` [#1874]
- Added the `spinner` and `spinner__base` parts to `<sl-tree-item>` [#1937]
- Added the `sync` property to `<sl-dropdown>` so the menu can easily sync sizes with the trigger element [#1935]
- Fixed a bug in `<sl-icon>` that did not properly apply mutators to spritesheets [#1927]
- Fixed a bug in `.sl-scroll-lock` causing layout shifts [#1895]
- Fixed a bug in `<sl-rating>` that caused the rating to not reset in some circumstances [#1877]
- Fixed a bug in `<sl-select>` that caused the menu to not close when rendered in a shadow root [#1878]
- Fixed a bug in `<sl-tree>` that caused a new stacking context resulting in tooltips being clipped [#1709]
- Fixed a bug in `<sl-tab-group>` that caused the scroll controls to toggle indefinitely when zoomed in Safari [#1839]
- Fixed a bug in the submenu controller that allowed two submenus to be open at the same time [#1880]
- Fixed a bug in `<sl-select>` where the tag size wouldn't update with the control's size [#1886]
- Fixed a bug in `<sl-checkbox>` and `<sl-switch>` where the color of the required content wasn't applying correctly
- Fixed a bug in `<sl-checkbox>` where help text was incorrectly styled [#1897]
- Fixed a bug in `<sl-input>` that prevented the control from receiving focus when clicking over the clear button
- Fixed a bug in `<sl-carousel>` that caused the carousel to be out of sync when used with reduced motion settings [#1887]
- Fixed a bug in `<sl-button-group>` that caused styles to stop working when using `className` on buttons in React [#1926]
## 2.14.0
- Added the Arabic translation [#1852]
- Added help text to `<sl-checkbox>` [#1860]
- Added help text to `<sl-switch>` [#1800]
- Fixed a bug in `<sl-option>` that caused HTML tags to be included in `getTextLabel()`
- Fixed a bug in `<sl-carousel>` that caused slides to not switch correctly [#1862]
- Refactored component styles to be consumed more efficiently [#1692]
## 2.13.1
- Fixed a bug where the safe triangle was always visible when selecting nested `<sl-menu>` elements [#1835]
## 2.13.0
- Added the `hover-bridge` feature to `<sl-popup>` to support better tooltip accessibility [#1734]
- Added the `loading` attribute and the `spinner` and `spinner__base` part to `<sl-menu-item>` [#1700]
- Fixed files that did not have `.js` extensions. [#1770]
- Fixed a bug in `<sl-tree>` when providing custom expand / collapse icons [#1922]
- Fixed `<sl-dialog>` not accounting for elements with hidden dialog controls like `<video>` [#1755]
- Fixed focus trapping not scrolling elements into view. [#1750]
- Fixed more performance issues with focus trapping performance. [#1750]
- Fixed a bug in `<sl-input>` and `<sl-textarea>` that made it work differently from `<input>` and `<textarea>` when using defaults [#1746]
- Fixed a bug in `<sl-select>` that prevented it from closing when tabbing to another select inside a shadow root [#1763]
- Fixed a bug in `<sl-spinner>` that caused the animation to appear strange in certain circumstances [#1787]
- Fixed a bug in `<sl-dialog>` with focus trapping [#1813]
- Fixed a bug that caused form controls to submit even after they were removed from the DOM [#1823]
- Fixed a bug that caused empty `<sl-radio-group>` elements to log an error in the console [#1795]
- Fixed a bug that caused modal scroll locking to conflict with the `scrollbar-gutter` property [#1805]
- Fixed a bug in `<sl-option>` that caused slotted content to show up when calling `getTextLabel()` [#1730]
- Fixed a bug in `<sl-color-picker>` that caused picker values to not match the preview color [#1831]
- Fixed a bug in `<sl-carousel>` where pagination dots don't update when swiping slide in iOS Safari [#1748]
- Fixed a bug in`<sl-carousel>` where trying to swipe doesn't change the slide in Firefox for Android [#1748]
- Improved the accessibility of `<sl-tooltip>` so they persist when hovering over the tooltip and dismiss when pressing [[Esc]] [#1734]
- Improved "close" behavior of multiple components in supportive browsers using the `CloseWatcher` API [#1788]
- Removed the scroll controller from the experimental `<sl-carousel>` and moved all mouse related logic into the component [#1748]
## 2.12.0
- Added the Italian translation [#1727]
- Added the ability to call `form.checkValidity()` and it will use Shoelace's custom `checkValidity()` handler. [#1708]
- Fixed a bug where nested dialogs were not properly trapping focus. [#1711]
- Fixed a bug with form controls removing the custom validity handlers from the form. [#1708]
- Fixed a bug in form control components that used a `form` property, but not an attribute. [#1707]
- Fixed a bug with bundled components using CDN builds not having translations on initial connect [#1696]
- Fixed a bug where the `"sl-change"` event would always fire simultaneously with `"sl-input"` event in `<sl-color-picker>`. The `<sl-change>` event now only fires when a user stops dragging a slider or stops dragging on the color canvas. [#1689]
- Updated the copy icon in the system library [#1702]
## 2.11.2
- Fixed a bug in `<sl-carousel>` component that caused an error to be thrown when rendered with Lit [#1684]
## 2.11.1
- Improved the experimental `<sl-carousel>` component [#1605]
## 2.11.0
- Added the Croatian translation [#1656]
- Fixed a bug that caused the [[Escape]] key to stop propagating when tooltips are disabled [#1607]
- Fixed a bug that made it impossible to style placeholders in `<sl-select>` [#1667]
- Fixed a bug that caused `dist/react/index.js` to be blank [#1659]
## 2.10.0
- Added the Simplified Chinese translation [#1604]
- Fixed a bug [in the localize dependency](https://github.com/shoelace-style/localize/issues/20) that caused underscores in language codes to throw a `RangeError`
- Fixed a bug in the focus trapping utility used by modals that caused unexpected focus behavior. [#1583]
- Fixed a bug in `<sl-copy-button>` that prevented exported tooltip parts from being styled [#1586]
- Fixed a bug in `<sl-menu>` that caused it not to fire the `sl-select` event if you clicked an element inside of a `<sl-menu-item>` [#1599]
- Fixed a bug that caused focus trap logic to hang the browser in certain circumstances [#1612]
- Improved submenu selection by implementing the [safe triangle](https://www.smashingmagazine.com/2023/08/better-context-menus-safe-triangles/) method [#1550]
- Updated `@shoelace-style/localize` to 3.1.0
- Updated `@lib-labs/react` to stable `@lit/react`
- Updated Bootstrap Icons to 1.11.1
- Updated Lit to 3.0.0
- Updated TypeScript to 5.2.2
- Updated all other dependencies to latest versions
## 2.9.0
- Added the `modal` property to `<sl-dialog>` and `<sl-drawer>` to support third-party modals [#1571]
- Fixed a bug in the autoloader causing it to register non-Shoelace elements [#1563]
- Fixed a bug in `<sl-switch>` that resulted in improper spacing between the label and the required asterisk [#1540]
- Fixed a bug in `<sl-icon>` that caused icons to not load when the default library used a sprite sheet [#1572]
- Removed error when a missing popup anchor is provided [#1548]
- Updated `@ctrl/tinycolor` to 4.0.1 [#1542]
- Updated Bootstrap Icons to 1.11.0
## 2.8.0
- Added `--isolatedModules` and `--verbatimModuleSyntax` to `tsconfig.json`. For anyone directly importing event types, they no longer provide a default export due to these options being enabled. For people using the `events/event.js` file directly, there is no change.
@ -23,7 +150,7 @@ New versions of Shoelace are released as-needed and generally occur when a criti
- Improved expand/collapse behavior of `<sl-tree>` to work more like users expect [#1521]
- Improved `<sl-menu-item>` so labels truncate properly instead of getting chopped and overflowing
- Removed the extra `React.Component` around `@lit-labs/react` wrapper. [#1531]
- Upgrade `@lit-labs/react` to v2.0.1. [#1531]
- Updated `@lit-labs/react` to v2.0.1. [#1531]
## 2.7.0

Wyświetl plik

@ -36,6 +36,7 @@ I realize that one cannot reasonably enforce this any more than one can enforce
The [issue tracker](https://github.com/shoelace-style/shoelace/issues) is for bug reports, feature requests, and pull requests.
- Please **do not** use the issue tracker for personal support requests. Use [the discussion forum](https://github.com/shoelace-style/shoelace/discussions/categories/help) instead.
- Please **do not** use the issue tracker for feature requests. Use [the discussion forum](https://github.com/shoelace-style/shoelace/discussions/categories/ideas) instead.
- Please **do not** derail, hijack, or troll issues. Keep the discussion on topic and be respectful of others.
- Please **do not** post comments with "+1" or "👍". Use [reactions](https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) instead.
- Please **do** use the issue tracker for feature requests, bug reports, and pull requests.
@ -44,15 +45,13 @@ Issues that do not follow these guidelines are subject to closure. There simply
### Feature Requests
Feature requests can be added using the issue tracker.
Feature requests can be added using [the discussion forum](https://github.com/shoelace-style/shoelace/discussions/categories/ideas).
- Please **do** search for an existing request before suggesting a new feature.
- Please **do** use the "👍" reaction to vote for a feature.
- Please **do** use the voting buttons to vote for a feature.
- Please **do** share substantial use cases and perspective that support new features if they haven't already been mentioned.
- Please **do not** bump, spam, or ping contributors to prioritize your own feature.
If you would like your feature prioritized, please consider [sponsoring the project](https://github.com/sponsors/claviska).
### Bug Reports
A bug is _a demonstrable problem_ caused by code in the library. Bug reports are an important contribution to the quality of the project. When submitting a bug report, there are a few steps you can take to make sure your issues gets attention quickly.
@ -65,8 +64,6 @@ A bug is _a demonstrable problem_ caused by code in the library. Bug reports are
**A minimal test case is critical to a successful bug report.** It demonstrates that the bug exists in the library and not in surrounding code. Contributors should be able to understand the bug without studying your code, otherwise they'll probably move on to another bug.
If you would like your bug prioritized, please consider [sponsoring the project](https://github.com/sponsors/claviska).
### Pull Requests
To keep the project on track, please consider the following guidelines before submitting a PR.

Wyświetl plik

@ -88,7 +88,7 @@ module.exports = environment;
The final step is to add the corresponding `pack_tags` to the page. You should have the following `tags` in the `<head>` section of `app/views/layouts/application.html.erb`.
```html
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<!-- ... -->

16975
package-lock.json wygenerowano

Plik diff jest za duży Load Diff

Wyświetl plik

@ -1,12 +1,12 @@
{
"name": "@shoelace-style/shoelace",
"description": "A forward-thinking library of web components.",
"version": "2.8.0",
"version": "2.15.1",
"homepage": "https://github.com/shoelace-style/shoelace",
"author": "Cory LaViska",
"license": "MIT",
"customElements": "dist/custom-elements.json",
"web-types": "./web-types.json",
"web-types": "./dist/web-types.json",
"type": "module",
"types": "dist/shoelace.d.ts",
"jsdelivr": "./cdn/shoelace-autoloader.js",
@ -49,12 +49,12 @@
"start": "node scripts/build.js --serve",
"build": "node scripts/build.js",
"verify": "npm run prettier:check && npm run lint && npm run build && npm run test",
"prepare": "npx playwright install",
"prepublishOnly": "npm run verify",
"prettier": "prettier --write --loglevel warn .",
"prettier:check": "prettier --check --loglevel warn .",
"prettier": "prettier --write --log-level=warn .",
"prettier:check": "prettier --check --log-level=warn .",
"lint": "eslint src --max-warnings 0",
"lint:fix": "eslint src --max-warnings 0 --fix",
"ts-check": "tsc --noEmit --project ./tsconfig.json",
"create": "plop --plopfile scripts/plop/plopfile.js",
"test": "web-test-runner --group default",
"test:component": "web-test-runner -- --watch --group",
@ -67,77 +67,78 @@
"node": ">=14.17.0"
},
"dependencies": {
"@ctrl/tinycolor": "^3.5.0",
"@floating-ui/dom": "^1.2.1",
"@lit-labs/react": "^2.0.1",
"@ctrl/tinycolor": "^4.0.2",
"@floating-ui/dom": "^1.5.3",
"@lit/react": "^1.0.0",
"@shoelace-style/animations": "^1.1.0",
"@shoelace-style/localize": "^3.1.1",
"@shoelace-style/localize": "^3.1.2",
"composed-offset-position": "^0.0.4",
"lit": "^2.7.5",
"lit": "^3.0.0",
"qr-creator": "^1.0.0"
},
"devDependencies": {
"@11ty/eleventy": "^2.0.1",
"@custom-elements-manifest/analyzer": "^0.8.3",
"@open-wc/testing": "^3.1.7",
"@types/mocha": "^10.0.1",
"@types/react": "^18.0.26",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@web/dev-server-esbuild": "^0.3.3",
"@web/test-runner": "^0.15.0",
"@web/test-runner-commands": "^0.6.5",
"@web/test-runner-playwright": "^0.9.0",
"bootstrap-icons": "^1.10.5",
"@custom-elements-manifest/analyzer": "^0.8.4",
"@open-wc/testing": "^3.2.0",
"@types/mocha": "^10.0.2",
"@types/react": "^18.2.28",
"@typescript-eslint/eslint-plugin": "^6.7.5",
"@typescript-eslint/parser": "^6.7.5",
"@web/dev-server-esbuild": "^0.3.6",
"@web/test-runner": "^0.18.0",
"@web/test-runner-commands": "^0.9.0",
"@web/test-runner-playwright": "^0.11.0",
"bootstrap-icons": "^1.11.1",
"browser-sync": "^2.29.3",
"chalk": "^5.2.0",
"chalk": "^5.3.0",
"change-case": "^4.1.2",
"command-line-args": "^5.2.1",
"comment-parser": "^1.3.1",
"comment-parser": "^1.4.0",
"cspell": "^6.18.1",
"custom-element-jet-brains-integration": "^1.1.0",
"custom-element-vs-code-integration": "^1.1.0",
"del": "^7.0.0",
"custom-element-jet-brains-integration": "^1.4.0",
"custom-element-vs-code-integration": "^1.2.1",
"custom-element-vuejs-integration": "^1.0.0",
"del": "^7.1.0",
"download": "^8.0.0",
"esbuild": "^0.18.2",
"esbuild": "^0.19.4",
"esbuild-plugin-replace": "^1.4.0",
"eslint": "^8.44.0",
"eslint": "^8.51.0",
"eslint-plugin-chai-expect": "^3.0.0",
"eslint-plugin-chai-friendly": "^0.7.2",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-lit": "^1.8.3",
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-lit": "^1.9.1",
"eslint-plugin-lit-a11y": "^4.1.0",
"eslint-plugin-markdown": "^3.0.0",
"eslint-plugin-markdown": "^3.0.1",
"eslint-plugin-sort-imports-es6-autofix": "^0.6.0",
"eslint-plugin-wc": "^1.5.0",
"eslint-plugin-wc": "^2.0.4",
"front-matter": "^4.0.2",
"get-port": "^7.0.0",
"globby": "^13.1.3",
"globby": "^13.2.2",
"husky": "^8.0.3",
"jsdom": "^22.1.0",
"jsonata": "^2.0.1",
"lint-staged": "^13.1.0",
"jsonata": "^2.0.3",
"lint-staged": "^14.0.1",
"lunr": "^2.3.9",
"markdown-it-container": "^3.0.0",
"markdown-it-ins": "^3.0.1",
"markdown-it-kbd": "^2.2.2",
"markdown-it-mark": "^3.0.1",
"markdown-it-replace-it": "^1.0.0",
"npm-check-updates": "^16.6.2",
"ora": "^6.3.1",
"npm-check-updates": "^16.14.6",
"ora": "^7.0.1",
"pascal-case": "^3.1.2",
"plop": "^3.1.1",
"prettier": "^2.8.8",
"plop": "^4.0.0",
"prettier": "^3.0.3",
"prismjs": "^1.29.0",
"react": "^18.2.0",
"recursive-copy": "^2.0.14",
"sinon": "^15.0.1",
"sinon": "^16.1.0",
"smartquotes": "^2.3.2",
"source-map": "^0.7.4",
"strip-css-comments": "^5.0.0",
"tslib": "^2.4.1",
"typescript": "^5.1.3",
"user-agent-data-types": "^0.3.0"
"tslib": "^2.6.2",
"typescript": "^5.2.2",
"user-agent-data-types": "^0.3.1"
},
"lint-staged": {
"*.{ts,js}": [

Wyświetl plik

@ -1,5 +1,5 @@
/* eslint-env node */
module.exports = {
/** @type {import("prettier").Config} */
const config = {
arrowParens: 'avoid',
bracketSpacing: true,
htmlWhitespaceSensitivity: 'css',
@ -16,3 +16,5 @@ module.exports = {
trailingComma: 'none',
useTabs: false
};
export default config;

Wyświetl plik

@ -34,6 +34,9 @@ const shoelaceVersion = JSON.stringify(packageData.version.toString());
async function buildTheDocs(watch = false) {
return new Promise(async (resolve, reject) => {
const afterSignal = '[eleventy.after]';
// Totally non-scientific way to handle errors. Perhaps its just better to resolve on stderr? :shrug:
const errorSignal = 'Original error stack trace:';
const args = ['@11ty/eleventy', '--quiet'];
const output = [];
@ -53,6 +56,10 @@ async function buildTheDocs(watch = false) {
output.push(data.toString());
});
child.stderr.on('data', data => {
output.push(data.toString());
});
if (watch) {
// The process doesn't terminate in watch mode so, before resolving, we listen for a known signal in stdout that
// tells us when the first build completes.
@ -61,6 +68,13 @@ async function buildTheDocs(watch = false) {
resolve({ child, output });
}
});
child.stderr.on('data', data => {
if (data.includes(errorSignal)) {
// This closes the dev server, not sure if thats what we want?
reject(output);
}
});
} else {
child.on('close', () => {
resolve({ child, output });
@ -73,7 +87,7 @@ async function buildTheDocs(watch = false) {
// Builds the source with esbuild.
//
async function buildTheSource() {
const alwaysExternal = ['@lit-labs/react', 'react'];
const alwaysExternal = ['@lit/react', 'react'];
const cdnConfig = {
format: 'esm',
@ -108,7 +122,7 @@ async function buildTheSource() {
// 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.)
//
// We never bundle React or @lit-labs/react though!
// We never bundle React or @lit/react though!
//
external: alwaysExternal,
splitting: true,
@ -201,9 +215,8 @@ await nextTask('Running the TypeScript compiler', () => {
});
// Copy the above steps to the CDN directory directly so we don't need to twice the work for nothing.
await nextTask(`Copying Web Types, Themes, Icons, and TS Types to "${cdndir}"`, async () => {
await nextTask(`Themes, Icons, and TS Types to "${cdndir}"`, async () => {
await deleteAsync(cdndir);
await copy('./web-types.json', `${outdir}/web-types.json`);
await copy(outdir, cdndir);
});

Wyświetl plik

@ -1,10 +1,9 @@
import commandLineArgs from 'command-line-args';
import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import { deleteSync } from 'del';
import prettier from 'prettier';
import prettierConfig from '../prettier.config.cjs';
import { default as prettierConfig } from '../prettier.config.js';
import { getAllComponents } from './shared.js';
const { outdir } = commandLineArgs({ name: 'outdir', type: String });
@ -20,19 +19,18 @@ const metadata = JSON.parse(fs.readFileSync(path.join(outdir, 'custom-elements.j
const components = getAllComponents(metadata);
const index = [];
components.map(component => {
for await (const component of components) {
const tagWithoutPrefix = component.tagName.replace(/^sl-/, '');
const componentDir = path.join(reactDir, tagWithoutPrefix);
const componentFile = path.join(componentDir, 'index.ts');
const importPath = component.path.replace(/\.js$/, '.component.js');
const eventImports = (component.events || [])
.map(event => `import type { ${event.eventName} } from '../../../src/events/events';`)
.map(event => `import type { ${event.eventName} } from '../../events/events.js';`)
.join('\n');
const eventExports = (component.events || [])
.map(event => `export type { ${event.eventName} } from '../../../src/events/events';`)
.map(event => `export type { ${event.eventName} } from '../../events/events.js';`)
.join('\n');
const eventNameImport =
(component.events || []).length > 0 ? `import { type EventName } from '@lit-labs/react';` : ``;
const eventNameImport = (component.events || []).length > 0 ? `import { type EventName } from '@lit/react';` : ``;
const events = (component.events || [])
.map(event => `${event.reactName}: '${event.name}' as EventName<${event.eventName}>`)
.join(',\n');
@ -41,10 +39,10 @@ components.map(component => {
const jsDoc = component.jsDoc || '';
const source = prettier.format(
const source = await prettier.format(
`
import * as React from 'react';
import { createComponent } from '@lit-labs/react';
import { createComponent } from '@lit/react';
import Component from '../../${importPath}';
${eventNameImport}
@ -75,7 +73,7 @@ components.map(component => {
index.push(`export { default as ${component.name} } from './${tagWithoutPrefix}/index.js';`);
fs.writeFileSync(componentFile, source, 'utf8');
});
}
// Generate the index file
fs.writeFileSync(path.join(reactDir, 'index.ts'), index.join('\n'), 'utf8');

Wyświetl plik

@ -24,7 +24,7 @@ filesToEmbed.forEach(file => {
});
// Loop through each theme file, copying the .css and generating a .js version for Lit users
files.forEach(file => {
files.forEach(async file => {
let source = fs.readFileSync(file, 'utf8');
// If the source has "/* _filename.css */" in it, replace it with the embedded styles
@ -32,11 +32,11 @@ files.forEach(file => {
source = source.replace(`/* ${key} */`, embeds[key]);
});
const css = prettier.format(stripComments(source), {
const css = await prettier.format(stripComments(source), {
parser: 'css'
});
let js = prettier.format(
let js = await prettier.format(
`
import { css } from 'lit';
@ -47,9 +47,19 @@ files.forEach(file => {
{ parser: 'babel-ts' }
);
let dTs = await prettier.format(
`
declare const _default: import("lit").CSSResult;
export default _default;
`,
{ parser: 'babel-ts' }
);
const cssFile = path.join(themesDir, path.basename(file));
const jsFile = path.join(themesDir, path.basename(file).replace('.css', '.styles.js'));
const dTsFile = path.join(themesDir, path.basename(file).replace('.css', '.styles.d.ts'));
fs.writeFileSync(cssFile, css, 'utf8');
fs.writeFileSync(jsFile, js, 'utf8');
fs.writeFileSync(dTsFile, dTs, 'utf8');
});

Wyświetl plik

@ -2,6 +2,7 @@ import { property } from 'lit/decorators.js';
import { html } from 'lit';
import { LocalizeController } from '../../utilities/localize.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import styles from './{{ tagWithoutPrefix tag }}.styles.js';
import type { CSSResultGroup } from 'lit';
@ -24,7 +25,7 @@ import type { CSSResultGroup } from 'lit';
* @cssproperty --example - An example CSS custom property.
*/
export default class {{ properCase tag }} extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static styles: CSSResultGroup = [componentStyles, styles];
private readonly localize = new LocalizeController(this);

Wyświetl plik

@ -1,9 +1,6 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles.js';
export default css`
${componentStyles}
:host {
display: block;
}

Wyświetl plik

@ -7,6 +7,7 @@ import { LocalizeController } from '../../utilities/localize.js';
import { property, query } from 'lit/decorators.js';
import { waitForEvent } from '../../internal/event.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import SlIconButton from '../icon-button/icon-button.component.js';
import styles from './alert.styles.js';
@ -40,7 +41,7 @@ const toastStack = Object.assign(document.createElement('div'), { className: 'sl
* @animation alert.hide - The animation to use when hiding the alert.
*/
export default class SlAlert extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static styles: CSSResultGroup = [componentStyles, styles];
static dependencies = { 'sl-icon-button': SlIconButton };
private autoHideTimeout: number;

Wyświetl plik

@ -1,9 +1,6 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles.js';
export default css`
${componentStyles}
:host {
display: contents;

Wyświetl plik

@ -210,10 +210,12 @@ describe('<sl-alert>', () => {
};
it('deletes the toast stack after the last alert is done', async () => {
const container = await fixture<HTMLElement>(html`<div>
<sl-alert data-testid="alert1" closable>alert 1</sl-alert>
<sl-alert data-testid="alert2" closable>alert 2</sl-alert>
</div>`);
const container = await fixture<HTMLElement>(
html`<div>
<sl-alert data-testid="alert1" closable>alert 1</sl-alert>
<sl-alert data-testid="alert2" closable>alert 2</sl-alert>
</div>`
);
const alert1 = queryByTestId<SlAlert>(container, 'alert1');
const alert2 = queryByTestId<SlAlert>(container, 'alert2');

Wyświetl plik

@ -1,6 +1,7 @@
import { html } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import SlIcon from '../icon/icon.component.js';
import styles from './animated-image.styles.js';
@ -20,13 +21,13 @@ import type { CSSResultGroup } from 'lit';
* @slot play-icon - Optional play icon to use instead of the default. Works best with `<sl-icon>`.
* @slot pause-icon - Optional pause icon to use instead of the default. Works best with `<sl-icon>`.
*
* @part - control-box - The container that surrounds the pause/play icons and provides their background.
* @part control-box - The container that surrounds the pause/play icons and provides their background.
*
* @cssproperty --control-box-size - The size of the icon box.
* @cssproperty --icon-size - The size of the play/pause icons.
*/
export default class SlAnimatedImage extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static styles: CSSResultGroup = [componentStyles, styles];
static dependencies = { 'sl-icon': SlIcon };
@query('.animated-image__animated') animatedImage: HTMLImageElement;

Wyświetl plik

@ -1,9 +1,6 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles.js';
export default css`
${componentStyles}
:host {
--control-box-size: 3rem;
--icon-size: calc(var(--control-box-size) * 0.625);

Wyświetl plik

@ -2,6 +2,7 @@ import { animations } from './animations.js';
import { html } from 'lit';
import { property, queryAsync } from 'lit/decorators.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import styles from './animation.styles.js';
import type { CSSResultGroup } from 'lit';
@ -20,7 +21,7 @@ import type { CSSResultGroup } from 'lit';
* animate multiple elements, either wrap them in a single container or use multiple `<sl-animation>` elements.
*/
export default class SlAnimation extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static styles: CSSResultGroup = [componentStyles, styles];
private animation?: Animation;
private hasStarted = false;

Wyświetl plik

@ -1,9 +1,6 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles.js';
export default css`
${componentStyles}
:host {
display: contents;
}

Wyświetl plik

@ -1,5 +1,6 @@
import '../../../dist/shoelace.js';
import { aTimeout, expect, fixture, html, oneEvent } from '@open-wc/testing';
import { aTimeout, expect, fixture, oneEvent } from '@open-wc/testing';
import { html } from 'lit';
import type SlAnimation from './animation.js';
describe('<sl-animation>', () => {

Wyświetl plik

@ -2,6 +2,7 @@ import { classMap } from 'lit/directives/class-map.js';
import { html } from 'lit';
import { property, state } from 'lit/decorators.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import SlIcon from '../icon/icon.component.js';
import styles from './avatar.styles.js';
@ -25,7 +26,7 @@ import type { CSSResultGroup } from 'lit';
* @cssproperty --size - The size of the avatar.
*/
export default class SlAvatar extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static styles: CSSResultGroup = [componentStyles, styles];
static dependencies = {
'sl-icon': SlIcon
};

Wyświetl plik

@ -1,9 +1,6 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles.js';
export default css`
${componentStyles}
:host {
display: inline-block;
@ -23,6 +20,7 @@ export default css`
font-weight: var(--sl-font-weight-normal);
color: var(--sl-color-neutral-0);
user-select: none;
-webkit-user-select: none;
vertical-align: middle;
}

Wyświetl plik

@ -1,6 +1,7 @@
import { classMap } from 'lit/directives/class-map.js';
import { html } from 'lit';
import { property } from 'lit/decorators.js';
import componentStyles from '../../styles/component.styles.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import styles from './badge.styles.js';
import type { CSSResultGroup } from 'lit';
@ -16,7 +17,7 @@ import type { CSSResultGroup } from 'lit';
* @csspart base - The component's base wrapper.
*/
export default class SlBadge extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static styles: CSSResultGroup = [componentStyles, styles];
/** The badge's theme variant. */
@property({ reflect: true }) variant: 'primary' | 'success' | 'neutral' | 'warning' | 'danger' = 'primary';

Wyświetl plik

@ -1,9 +1,6 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles.js';
export default css`
${componentStyles}
:host {
display: inline-flex;
}
@ -21,6 +18,7 @@ export default css`
white-space: nowrap;
padding: 0.35em 0.6em;
user-select: none;
-webkit-user-select: none;
cursor: inherit;
}

Wyświetl plik

@ -3,6 +3,7 @@ import { HasSlotController } from '../../internal/slot.js';
import { html } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js';
import { property } from 'lit/decorators.js';
import componentStyles from '../../styles/component.styles.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import styles from './breadcrumb-item.styles.js';
import type { CSSResultGroup } from 'lit';
@ -26,7 +27,7 @@ import type { CSSResultGroup } from 'lit';
* @csspart separator - The container that wraps the separator.
*/
export default class SlBreadcrumbItem extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static styles: CSSResultGroup = [componentStyles, styles];
private readonly hasSlotController = new HasSlotController(this, 'prefix', 'suffix');

Wyświetl plik

@ -1,9 +1,6 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles.js';
export default css`
${componentStyles}
:host {
display: inline-flex;
}
@ -84,5 +81,6 @@ export default css`
align-items: center;
margin: 0 var(--sl-spacing-x-small);
user-select: none;
-webkit-user-select: none;
}
`;

Wyświetl plik

@ -1,6 +1,7 @@
import { html } from 'lit';
import { LocalizeController } from '../../utilities/localize.js';
import { property, query } from 'lit/decorators.js';
import componentStyles from '../../styles/component.styles.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import SlIcon from '../icon/icon.component.js';
import styles from './breadcrumb.styles.js';
@ -21,7 +22,7 @@ import type SlBreadcrumbItem from '../breadcrumb-item/breadcrumb-item.js';
* @csspart base - The component's base wrapper.
*/
export default class SlBreadcrumb extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static styles: CSSResultGroup = [componentStyles, styles];
static dependencies = { 'sl-icon': SlIcon };
private readonly localize = new LocalizeController(this);

Wyświetl plik

@ -1,9 +1,6 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles.js';
export default css`
${componentStyles}
.breadcrumb {
display: flex;
align-items: center;

Wyświetl plik

@ -1,5 +1,6 @@
import { html } from 'lit';
import { property, query, state } from 'lit/decorators.js';
import componentStyles from '../../styles/component.styles.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import styles from './button-group.styles.js';
import type { CSSResultGroup } from 'lit';
@ -15,7 +16,7 @@ import type { CSSResultGroup } from 'lit';
* @csspart base - The component's base wrapper.
*/
export default class SlButtonGroup extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static styles: CSSResultGroup = [componentStyles, styles];
@query('slot') defaultSlot: HTMLSlotElement;
@ -29,22 +30,22 @@ export default class SlButtonGroup extends ShoelaceElement {
private handleFocus(event: Event) {
const button = findButton(event.target as HTMLElement);
button?.classList.add('sl-button-group__button--focus');
button?.toggleAttribute('data-sl-button-group__button--focus', true);
}
private handleBlur(event: Event) {
const button = findButton(event.target as HTMLElement);
button?.classList.remove('sl-button-group__button--focus');
button?.toggleAttribute('data-sl-button-group__button--focus', false);
}
private handleMouseOver(event: Event) {
const button = findButton(event.target as HTMLElement);
button?.classList.add('sl-button-group__button--hover');
button?.toggleAttribute('data-sl-button-group__button--hover', true);
}
private handleMouseOut(event: Event) {
const button = findButton(event.target as HTMLElement);
button?.classList.remove('sl-button-group__button--hover');
button?.toggleAttribute('data-sl-button-group__button--hover', false);
}
private handleSlotChange() {
@ -55,11 +56,14 @@ export default class SlButtonGroup extends ShoelaceElement {
const button = findButton(el);
if (button) {
button.classList.add('sl-button-group__button');
button.classList.toggle('sl-button-group__button--first', index === 0);
button.classList.toggle('sl-button-group__button--inner', index > 0 && index < slottedElements.length - 1);
button.classList.toggle('sl-button-group__button--last', index === slottedElements.length - 1);
button.classList.toggle('sl-button-group__button--radio', button.tagName.toLowerCase() === 'sl-radio-button');
button.toggleAttribute('data-sl-button-group__button', true);
button.toggleAttribute('data-sl-button-group__button--first', index === 0);
button.toggleAttribute('data-sl-button-group__button--inner', index > 0 && index < slottedElements.length - 1);
button.toggleAttribute('data-sl-button-group__button--last', index === slottedElements.length - 1);
button.toggleAttribute(
'data-sl-button-group__button--radio',
button.tagName.toLowerCase() === 'sl-radio-button'
);
}
});
}

Wyświetl plik

@ -1,9 +1,6 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles.js';
export default css`
${componentStyles}
:host {
display: inline-block;
}

Wyświetl plik

@ -27,8 +27,8 @@ describe('<sl-button-group>', () => {
});
});
describe('slotted button classes', () => {
it('slotted buttons have the right classes applied based on their order', async () => {
describe('slotted button data attributes', () => {
it('slotted buttons have the right data attributes applied based on their order', async () => {
const group = await fixture<SlButtonGroup>(html`
<sl-button-group>
<sl-button>Button 1 Label</sl-button>
@ -38,19 +38,19 @@ describe('<sl-button-group>', () => {
`);
const allButtons = group.querySelectorAll('sl-button');
const hasGroupClass = Array.from(allButtons).every(button =>
button.classList.contains('sl-button-group__button')
const hasGroupAttrib = Array.from(allButtons).every(button =>
button.hasAttribute('data-sl-button-group__button')
);
expect(hasGroupClass).to.be.true;
expect(hasGroupAttrib).to.be.true;
expect(allButtons[0]).to.have.class('sl-button-group__button--first');
expect(allButtons[1]).to.have.class('sl-button-group__button--inner');
expect(allButtons[2]).to.have.class('sl-button-group__button--last');
expect(allButtons[0]).to.have.attribute('data-sl-button-group__button--first');
expect(allButtons[1]).to.have.attribute('data-sl-button-group__button--inner');
expect(allButtons[2]).to.have.attribute('data-sl-button-group__button--last');
});
});
describe('focus and blur events', () => {
it('toggles focus class to slotted buttons on focus/blur', async () => {
it('toggles focus data attribute to slotted buttons on focus/blur', async () => {
const group = await fixture<SlButtonGroup>(html`
<sl-button-group>
<sl-button>Button 1 Label</sl-button>
@ -63,16 +63,16 @@ describe('<sl-button-group>', () => {
allButtons[0].dispatchEvent(new FocusEvent('focusin', { bubbles: true }));
await elementUpdated(allButtons[0]);
expect(allButtons[0].classList.contains('sl-button-group__button--focus')).to.be.true;
expect(allButtons[0]).to.have.attribute('data-sl-button-group__button--focus');
allButtons[0].dispatchEvent(new FocusEvent('focusout', { bubbles: true }));
await elementUpdated(allButtons[0]);
expect(allButtons[0].classList.contains('sl-button-group__button--focus')).not.to.be.true;
expect(allButtons[0]).to.not.have.attribute('data-sl-button-group__button--focus');
});
});
describe('mouseover and mouseout events', () => {
it('toggles hover class to slotted buttons on mouseover/mouseout', async () => {
it('toggles hover data attribute to slotted buttons on mouseover/mouseout', async () => {
const group = await fixture<SlButtonGroup>(html`
<sl-button-group>
<sl-button>Button 1 Label</sl-button>
@ -85,11 +85,12 @@ describe('<sl-button-group>', () => {
allButtons[0].dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
await elementUpdated(allButtons[0]);
expect(allButtons[0].classList.contains('sl-button-group__button--hover')).to.be.true;
expect(allButtons[0]).to.have.attribute('data-sl-button-group__button--hover');
allButtons[0].dispatchEvent(new MouseEvent('mouseout', { bubbles: true }));
await elementUpdated(allButtons[0]);
expect(allButtons[0].classList.contains('sl-button-group__button--hover')).not.to.be.true;
console.log(allButtons[0]);
expect(allButtons[0]).to.not.have.attribute('data-sl-button-group__button--hover');
});
});
});

Wyświetl plik

@ -6,6 +6,7 @@ import { ifDefined } from 'lit/directives/if-defined.js';
import { LocalizeController } from '../../utilities/localize.js';
import { property, query, state } from 'lit/decorators.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import SlIcon from '../icon/icon.component.js';
import SlSpinner from '../spinner/spinner.component.js';
@ -38,27 +39,16 @@ import type { ShoelaceFormControl } from '../../internal/shoelace-element.js';
* @csspart spinner - The spinner that shows when the button is in the loading state.
*/
export default class SlButton extends ShoelaceElement implements ShoelaceFormControl {
static styles: CSSResultGroup = styles;
static styles: CSSResultGroup = [componentStyles, styles];
static dependencies = {
'sl-icon': SlIcon,
'sl-spinner': SlSpinner
};
private readonly formControlController = new FormControlController(this, {
form: input => {
// Buttons support a form attribute that points to an arbitrary form, so if this attribute is set we need to query
// the form from the same root using its id
if (input.hasAttribute('form')) {
const doc = input.getRootNode() as Document | ShadowRoot;
const formId = input.getAttribute('form')!;
return doc.getElementById(formId) as HTMLFormElement;
}
// Fall back to the closest containing form
return input.closest('form');
},
assumeInteractionOn: ['click']
});
private readonly hasSlotController = new HasSlotController(this, '[default]', 'prefix', 'suffix');
private readonly localize = new LocalizeController(this);

Wyświetl plik

@ -1,9 +1,6 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles.js';
export default css`
${componentStyles}
:host {
display: inline-block;
position: relative;
@ -22,11 +19,15 @@ export default css`
font-weight: var(--sl-font-weight-semibold);
text-decoration: none;
user-select: none;
-webkit-user-select: none;
white-space: nowrap;
vertical-align: middle;
padding: 0;
transition: var(--sl-transition-x-fast) background-color, var(--sl-transition-x-fast) color,
var(--sl-transition-x-fast) border, var(--sl-transition-x-fast) box-shadow;
transition:
var(--sl-transition-x-fast) background-color,
var(--sl-transition-x-fast) color,
var(--sl-transition-x-fast) border,
var(--sl-transition-x-fast) box-shadow;
cursor: inherit;
}
@ -545,30 +546,30 @@ export default css`
* buttons and we style them here instead.
*/
:host(.sl-button-group__button--first:not(.sl-button-group__button--last)) .button {
:host([data-sl-button-group__button--first]:not([data-sl-button-group__button--last])) .button {
border-start-end-radius: 0;
border-end-end-radius: 0;
}
:host(.sl-button-group__button--inner) .button {
:host([data-sl-button-group__button--inner]) .button {
border-radius: 0;
}
:host(.sl-button-group__button--last:not(.sl-button-group__button--first)) .button {
:host([data-sl-button-group__button--last]:not([data-sl-button-group__button--first])) .button {
border-start-start-radius: 0;
border-end-start-radius: 0;
}
/* All except the first */
:host(.sl-button-group__button:not(.sl-button-group__button--first)) {
:host([data-sl-button-group__button]:not([data-sl-button-group__button--first])) {
margin-inline-start: calc(-1 * var(--sl-input-border-width));
}
/* Add a visual separator between solid buttons */
:host(
.sl-button-group__button:not(
.sl-button-group__button--first,
.sl-button-group__button--radio,
[data-sl-button-group__button]:not(
[data-sl-button-group__button--first],
[data-sl-button-group__button--radio],
[variant='default']
):not(:hover)
)
@ -583,13 +584,13 @@ export default css`
}
/* Bump hovered, focused, and checked buttons up so their focus ring isn't clipped */
:host(.sl-button-group__button--hover) {
:host([data-sl-button-group__button--hover]) {
z-index: 1;
}
/* Focus and checked are always on top */
:host(.sl-button-group__button--focus),
:host(.sl-button-group__button[checked]) {
:host([data-sl-button-group__button--focus]),
:host([data-sl-button-group__button][checked]) {
z-index: 2;
}
`;

Wyświetl plik

@ -99,25 +99,25 @@ describe('<sl-button>', () => {
});
it('should render a link with rel="noreferrer noopener" when target is set and rel is not', async () => {
const el = await fixture<SlButton>(
html` <sl-button href="https://example.com/" target="_blank">Link</sl-button> `
);
const el = await fixture<SlButton>(html`
<sl-button href="https://example.com/" target="_blank">Link</sl-button>
`);
const link = el.shadowRoot!.querySelector('a')!;
expect(link?.getAttribute('rel')).to.equal('noreferrer noopener');
});
it('should render a link with rel="" when a target is provided and rel is empty', async () => {
const el = await fixture<SlButton>(
html` <sl-button href="https://example.com/" target="_blank" rel="">Link</sl-button> `
);
const el = await fixture<SlButton>(html`
<sl-button href="https://example.com/" target="_blank" rel="">Link</sl-button>
`);
const link = el.shadowRoot!.querySelector('a')!;
expect(link?.getAttribute('rel')).to.equal('');
});
it(`should render a link with a custom rel when a custom rel is provided`, async () => {
const el = await fixture<SlButton>(
html` <sl-button href="https://example.com/" target="_blank" rel="1">Link</sl-button> `
);
const el = await fixture<SlButton>(html`
<sl-button href="https://example.com/" target="_blank" rel="1">Link</sl-button>
`);
const link = el.shadowRoot!.querySelector('a')!;
expect(link?.getAttribute('rel')).to.equal('1');
});

Wyświetl plik

@ -1,6 +1,7 @@
import { classMap } from 'lit/directives/class-map.js';
import { HasSlotController } from '../../internal/slot.js';
import { html } from 'lit';
import componentStyles from '../../styles/component.styles.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import styles from './card.styles.js';
import type { CSSResultGroup } from 'lit';
@ -28,7 +29,7 @@ import type { CSSResultGroup } from 'lit';
* @cssproperty --padding - The padding to use for the card's sections.
*/
export default class SlCard extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static styles: CSSResultGroup = [componentStyles, styles];
private readonly hasSlotController = new HasSlotController(this, 'footer', 'header', 'image');

Wyświetl plik

@ -1,9 +1,6 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles.js';
export default css`
${componentStyles}
:host {
--border-color: var(--sl-color-neutral-200);
--border-radius: var(--sl-border-radius-medium);

Wyświetl plik

@ -7,9 +7,9 @@ describe('<sl-card>', () => {
describe('when provided no parameters', () => {
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> `
);
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 pass accessibility tests', async () => {

Wyświetl plik

@ -1,4 +1,5 @@
import { html } from 'lit';
import componentStyles from '../../styles/component.styles.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import styles from './carousel-item.styles.js';
import type { CSSResultGroup } from 'lit';
@ -15,11 +16,7 @@ import type { CSSResultGroup } from 'lit';
*
*/
export default class SlCarouselItem extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static isCarouselItem(node: Node) {
return node instanceof Element && node.getAttribute('aria-roledescription') === 'slide';
}
static styles: CSSResultGroup = [componentStyles, styles];
connectedCallback() {
super.connectedCallback();

Wyświetl plik

@ -1,9 +1,6 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles.js';
export default css`
${componentStyles}
:host {
--aspect-ratio: inherit;
@ -19,8 +16,8 @@ export default css`
}
::slotted(img) {
width: 100%;
height: 100%;
width: 100% !important;
height: 100% !important;
object-fit: cover;
}
`;

Wyświetl plik

@ -10,7 +10,7 @@ describe('<sl-carousel-item>', () => {
it('should pass accessibility tests', async () => {
// Arrange
const el = await fixture(html` <div role="list"><sl-carousel-item></sl-carousel-item></div> `);
const el = await fixture(html` <sl-carousel-item></sl-carousel-item> `);
// Assert
await expect(el).to.be.accessible();

Wyświetl plik

@ -1,19 +1,22 @@
import '../../internal/scrollend-polyfill.js';
import { AutoplayController } from './autoplay-controller.js';
import { clamp } from '../../internal/math.js';
import { classMap } from 'lit/directives/class-map.js';
import { eventOptions, property, query, state } from 'lit/decorators.js';
import { html } from 'lit';
import { LocalizeController } from '../../utilities/localize.js';
import { map } from 'lit/directives/map.js';
import { prefersReducedMotion } from '../../internal/animate.js';
import { property, query, state } from 'lit/decorators.js';
import { range } from 'lit/directives/range.js';
import { ScrollController } from './scroll-controller.js';
import { waitForEvent } from '../../internal/event.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import SlCarouselItem from '../carousel-item/carousel-item.component.js';
import SlIcon from '../icon/icon.component.js';
import styles from './carousel.styles.js';
import type { CSSResultGroup } from 'lit';
import type { CSSResultGroup, PropertyValueMap } from 'lit';
import type SlCarouselItem from '../carousel-item/carousel-item.component.js';
/**
* @summary Carousels display an arbitrary number of content slides along a horizontal or vertical axis.
@ -40,12 +43,12 @@ import type { CSSResultGroup } from 'lit';
* @csspart navigation-button--next - Applied to the next button.
*
* @cssproperty --slide-gap - The space between each slide.
* @cssproperty --aspect-ratio - The aspect ratio of each slide.
* @cssproperty [--aspect-ratio=16/9] - The aspect ratio of each slide.
* @cssproperty --scroll-hint - The amount of padding to apply to the scroll area, allowing adjacent slides to become
* partially visible as a scroll hint.
*/
export default class SlCarousel extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static styles: CSSResultGroup = [componentStyles, styles];
static dependencies = { 'sl-icon': SlIcon };
/** When set, allows the user to navigate the carousel in the same direction indefinitely. */
@ -68,7 +71,7 @@ export default class SlCarousel extends ShoelaceElement {
/**
* Specifies the number of slides the carousel will advance when scrolling, useful when specifying a `slides-per-page`
* greater than one.
* greater than one. It can't be higher than `slides-per-page`.
*/
@property({ type: Number, attribute: 'slides-per-move' }) slidesPerMove = 1;
@ -78,19 +81,17 @@ export default class SlCarousel extends ShoelaceElement {
/** When set, it is possible to scroll through the slides by dragging them with the mouse. */
@property({ type: Boolean, reflect: true, attribute: 'mouse-dragging' }) mouseDragging = false;
@query('slot:not([name])') defaultSlot: HTMLSlotElement;
@query('.carousel__slides') scrollContainer: HTMLElement;
@query('.carousel__pagination') paginationContainer: HTMLElement;
// The index of the active slide
@state() activeSlide = 0;
@state() scrolling = false;
@state() dragging = false;
private autoplayController = new AutoplayController(this, () => this.next());
private scrollController = new ScrollController(this);
private readonly slides = this.getElementsByTagName('sl-carousel-item');
private intersectionObserver: IntersectionObserver; // determines which slide is displayed
// A map containing the state of all the slides
private readonly intersectionObserverEntries = new Map<Element, IntersectionObserverEntry>();
private readonly localize = new LocalizeController(this);
private mutationObserver: MutationObserver;
@ -98,60 +99,61 @@ export default class SlCarousel extends ShoelaceElement {
super.connectedCallback();
this.setAttribute('role', 'region');
this.setAttribute('aria-label', this.localize.term('carousel'));
const intersectionObserver = new IntersectionObserver(
(entries: IntersectionObserverEntry[]) => {
entries.forEach(entry => {
// Store all the entries in a map to be processed when scrolling ends
this.intersectionObserverEntries.set(entry.target, entry);
const slide = entry.target;
slide.toggleAttribute('inert', !entry.isIntersecting);
slide.classList.toggle('--in-view', entry.isIntersecting);
slide.setAttribute('aria-hidden', entry.isIntersecting ? 'false' : 'true');
});
},
{
root: this,
threshold: 0.6
}
);
this.intersectionObserver = intersectionObserver;
// Store the initial state of each slide
intersectionObserver.takeRecords().forEach(entry => {
this.intersectionObserverEntries.set(entry.target, entry);
});
}
disconnectedCallback(): void {
super.disconnectedCallback();
this.intersectionObserver.disconnect();
this.mutationObserver.disconnect();
}
protected firstUpdated(): void {
this.initializeSlides();
this.mutationObserver = new MutationObserver(this.handleSlotChange);
this.mutationObserver.observe(this, { childList: true, subtree: false });
this.mutationObserver.observe(this, {
childList: true,
subtree: true
});
}
protected willUpdate(changedProperties: PropertyValueMap<SlCarousel> | Map<PropertyKey, unknown>): void {
// Ensure the slidesPerMove is never higher than the slidesPerPage
if (changedProperties.has('slidesPerMove') || changedProperties.has('slidesPerPage')) {
this.slidesPerMove = Math.min(this.slidesPerMove, this.slidesPerPage);
}
}
private getPageCount() {
return Math.ceil(this.getSlides().length / this.slidesPerPage);
const slidesCount = this.getSlides().length;
const { slidesPerPage, slidesPerMove, loop } = this;
const pages = loop ? slidesCount / slidesPerMove : (slidesCount - slidesPerPage) / slidesPerMove + 1;
return Math.ceil(pages);
}
private getCurrentPage() {
return Math.ceil(this.activeSlide / this.slidesPerPage);
return Math.ceil(this.activeSlide / this.slidesPerMove);
}
private canScrollNext(): boolean {
return this.loop || this.getCurrentPage() < this.getPageCount() - 1;
}
private canScrollPrev(): boolean {
return this.loop || this.getCurrentPage() > 0;
}
/** @internal Gets all carousel items. */
private getSlides({ excludeClones = true }: { excludeClones?: boolean } = {}) {
return [...this.slides].filter(slide => !excludeClones || !slide.hasAttribute('data-clone'));
return [...this.children].filter(
(el: HTMLElement) => this.isCarouselItem(el) && (!excludeClones || !el.hasAttribute('data-clone'))
) as SlCarouselItem[];
}
private handleKeyDown(event: KeyboardEvent) {
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) {
const target = event.target as HTMLElement;
const isRtl = this.localize.dir() === 'rtl';
const isRtl = this.matches(':dir(rtl)');
const isFocusInPagination = target.closest('[part~="pagination-item"]') !== null;
const isNext =
event.key === 'ArrowDown' || (!isRtl && event.key === 'ArrowRight') || (isRtl && event.key === 'ArrowLeft');
@ -190,31 +192,135 @@ export default class SlCarousel extends ShoelaceElement {
}
}
private handleMouseDragStart(event: PointerEvent) {
const canDrag = this.mouseDragging && event.button === 0;
if (canDrag) {
event.preventDefault();
document.addEventListener('pointermove', this.handleMouseDrag, { capture: true, passive: true });
document.addEventListener('pointerup', this.handleMouseDragEnd, { capture: true, once: true });
}
}
private handleMouseDrag = (event: PointerEvent) => {
if (!this.dragging) {
// Start dragging if it hasn't yet
this.scrollContainer.style.setProperty('scroll-snap-type', 'none');
this.dragging = true;
}
this.scrollContainer.scrollBy({
left: -event.movementX,
top: -event.movementY,
behavior: 'instant'
});
};
private handleMouseDragEnd = () => {
const scrollContainer = this.scrollContainer;
document.removeEventListener('pointermove', this.handleMouseDrag, { capture: true });
// get the current scroll position
const startLeft = scrollContainer.scrollLeft;
const startTop = scrollContainer.scrollTop;
// remove the scroll-snap-type property so that the browser will snap the slide to the correct position
scrollContainer.style.removeProperty('scroll-snap-type');
// fix(safari): forcing a style recalculation doesn't seem to immediately update the scroll
// position in Safari. Setting "overflow" to "hidden" should force this behavior.
scrollContainer.style.setProperty('overflow', 'hidden');
// get the final scroll position to the slide snapped by the browser
const finalLeft = scrollContainer.scrollLeft;
const finalTop = scrollContainer.scrollTop;
// restore the scroll position to the original one, so that it can be smoothly animated if needed
scrollContainer.style.removeProperty('overflow');
scrollContainer.style.setProperty('scroll-snap-type', 'none');
scrollContainer.scrollTo({ left: startLeft, top: startTop, behavior: 'instant' });
requestAnimationFrame(async () => {
if (startLeft !== finalLeft || startTop !== finalTop) {
scrollContainer.scrollTo({
left: finalLeft,
top: finalTop,
behavior: prefersReducedMotion() ? 'auto' : 'smooth'
});
await waitForEvent(scrollContainer, 'scrollend');
}
scrollContainer.style.removeProperty('scroll-snap-type');
this.dragging = false;
this.handleScrollEnd();
});
};
@eventOptions({ passive: true })
private handleScroll() {
this.scrolling = true;
}
/** @internal Synchronizes the slides with the IntersectionObserver API. */
private synchronizeSlides() {
const io = new IntersectionObserver(
entries => {
io.disconnect();
for (const entry of entries) {
const slide = entry.target;
slide.toggleAttribute('inert', !entry.isIntersecting);
slide.classList.toggle('--in-view', entry.isIntersecting);
slide.setAttribute('aria-hidden', entry.isIntersecting ? 'false' : 'true');
}
const firstIntersecting = entries.find(entry => entry.isIntersecting);
if (firstIntersecting) {
if (this.loop && firstIntersecting.target.hasAttribute('data-clone')) {
const clonePosition = Number(firstIntersecting.target.getAttribute('data-clone'));
// Scrolls to the original slide without animating, so the user won't notice that the position has changed
this.goToSlide(clonePosition, 'instant');
} else {
const slides = this.getSlides();
// Update the current index based on the first visible slide
const slideIndex = slides.indexOf(firstIntersecting.target as SlCarouselItem);
// Set the index to the first "snappable" slide
this.activeSlide = Math.ceil(slideIndex / this.slidesPerMove) * this.slidesPerMove;
}
}
},
{
root: this.scrollContainer,
threshold: 0.6
}
);
this.getSlides({ excludeClones: false }).forEach(slide => {
io.observe(slide);
});
}
private handleScrollEnd() {
const slides = this.getSlides();
const entries = [...this.intersectionObserverEntries.values()];
if (!this.scrolling || this.dragging) return;
const firstIntersecting: IntersectionObserverEntry | undefined = entries.find(entry => entry.isIntersecting);
this.synchronizeSlides();
if (this.loop && firstIntersecting?.target.hasAttribute('data-clone')) {
const clonePosition = Number(firstIntersecting.target.getAttribute('data-clone'));
this.scrolling = false;
}
// Scrolls to the original slide without animating, so the user won't notice that the position has changed
this.goToSlide(clonePosition, 'auto');
return;
}
// Activate the first intersecting slide
if (firstIntersecting) {
this.activeSlide = slides.indexOf(firstIntersecting.target as SlCarouselItem);
}
private isCarouselItem(node: Node): node is SlCarouselItem {
return node instanceof Element && node.tagName.toLowerCase() === 'sl-carousel-item';
}
private handleSlotChange = (mutations: MutationRecord[]) => {
const needsInitialization = mutations.some(mutation =>
[...mutation.addedNodes, ...mutation.removedNodes].some(
node => SlCarouselItem.isCarouselItem(node) && !(node as HTMLElement).hasAttribute('data-clone')
(el: HTMLElement) => this.isCarouselItem(el) && !el.hasAttribute('data-clone')
)
);
@ -222,21 +328,15 @@ export default class SlCarousel extends ShoelaceElement {
if (needsInitialization) {
this.initializeSlides();
}
this.requestUpdate();
};
@watch('loop', { waitUntilFirstUpdate: true })
@watch('slidesPerPage', { waitUntilFirstUpdate: true })
initializeSlides() {
const slides = this.getSlides();
const intersectionObserver = this.intersectionObserver;
this.intersectionObserverEntries.clear();
// Removes all the cloned elements from the carousel
this.getSlides({ excludeClones: false }).forEach((slide, index) => {
intersectionObserver.unobserve(slide);
slide.classList.remove('--in-view');
slide.classList.remove('--is-active');
slide.setAttribute('aria-label', this.localize.term('slideNum', index + 1));
@ -246,33 +346,39 @@ export default class SlCarousel extends ShoelaceElement {
}
});
this.updateSlidesSnap();
if (this.loop) {
// Creates clones to be placed before and after the original elements to simulate infinite scrolling
const slidesPerPage = this.slidesPerPage;
const lastSlides = slides.slice(-slidesPerPage);
const firstSlides = slides.slice(0, slidesPerPage);
lastSlides.reverse().forEach((slide, i) => {
const clone = slide.cloneNode(true) as HTMLElement;
clone.setAttribute('data-clone', String(slides.length - i - 1));
this.prepend(clone);
});
firstSlides.forEach((slide, i) => {
const clone = slide.cloneNode(true) as HTMLElement;
clone.setAttribute('data-clone', String(i));
this.append(clone);
});
this.createClones();
}
this.getSlides({ excludeClones: false }).forEach(slide => {
intersectionObserver.observe(slide);
});
this.synchronizeSlides();
// Because the DOM may be changed, restore the scroll position to the active slide
this.goToSlide(this.activeSlide, 'auto');
}
private createClones() {
const slides = this.getSlides();
const slidesPerPage = this.slidesPerPage;
const lastSlides = slides.slice(-slidesPerPage);
const firstSlides = slides.slice(0, slidesPerPage);
lastSlides.reverse().forEach((slide, i) => {
const clone = slide.cloneNode(true) as HTMLElement;
clone.setAttribute('data-clone', String(slides.length - i - 1));
this.prepend(clone);
});
firstSlides.forEach((slide, i) => {
const clone = slide.cloneNode(true) as HTMLElement;
clone.setAttribute('data-clone', String(i));
this.append(clone);
});
}
@watch('activeSlide')
handelSlideChange() {
const slides = this.getSlides();
@ -292,12 +398,12 @@ export default class SlCarousel extends ShoelaceElement {
}
@watch('slidesPerMove')
handleSlidesPerMoveChange() {
const slides = this.getSlides({ excludeClones: false });
updateSlidesSnap() {
const slides = this.getSlides();
const slidesPerMove = this.slidesPerMove;
slides.forEach((slide, i) => {
const shouldSnap = Math.abs(i - slidesPerMove) % slidesPerMove === 0;
const shouldSnap = (i + slidesPerMove) % slidesPerMove === 0;
if (shouldSnap) {
slide.style.removeProperty('scroll-snap-align');
} else {
@ -314,26 +420,13 @@ export default class SlCarousel extends ShoelaceElement {
}
}
@watch('mouseDragging')
handleMouseDraggingChange() {
this.scrollController.mouseDragging = this.mouseDragging;
}
/**
* Move the carousel backward by `slides-per-move` slides.
*
* @param behavior - The behavior used for scrolling.
*/
previous(behavior: ScrollBehavior = 'smooth') {
let previousIndex = this.activeSlide || this.activeSlide - this.slidesPerMove;
let canSnap = false;
while (!canSnap && previousIndex > 0) {
previousIndex -= 1;
canSnap = Math.abs(previousIndex - this.slidesPerMove) % this.slidesPerMove === 0;
}
this.goToSlide(previousIndex, behavior);
this.goToSlide(this.activeSlide - this.slidesPerMove, behavior);
}
/**
@ -352,13 +445,18 @@ export default class SlCarousel extends ShoelaceElement {
* @param behavior - The behavior used for scrolling.
*/
goToSlide(index: number, behavior: ScrollBehavior = 'smooth') {
const { slidesPerPage, loop, scrollContainer } = this;
const { slidesPerPage, loop } = this;
const slides = this.getSlides();
const slidesWithClones = this.getSlides({ excludeClones: false });
// No need to do anything in case there are no items in the carousel
if (!slides.length) {
return;
}
// Sets the next index without taking into account clones, if any.
const newActiveSlide = (index + slides.length) % slides.length;
const newActiveSlide = loop ? (index + slides.length) % slides.length : clamp(index, 0, slides.length - 1);
this.activeSlide = newActiveSlide;
// Get the index of the next slide. For looping carousel it adds `slidesPerPage`
@ -366,23 +464,31 @@ export default class SlCarousel extends ShoelaceElement {
const nextSlideIndex = clamp(index + (loop ? slidesPerPage : 0), 0, slidesWithClones.length - 1);
const nextSlide = slidesWithClones[nextSlideIndex];
this.scrollToSlide(nextSlide, prefersReducedMotion() ? 'auto' : behavior);
}
private scrollToSlide(slide: HTMLElement, behavior: ScrollBehavior = 'smooth') {
const scrollContainer = this.scrollContainer;
const scrollContainerRect = scrollContainer.getBoundingClientRect();
const nextSlideRect = nextSlide.getBoundingClientRect();
const nextSlideRect = slide.getBoundingClientRect();
const nextLeft = nextSlideRect.left - scrollContainerRect.left;
const nextTop = nextSlideRect.top - scrollContainerRect.top;
scrollContainer.scrollTo({
left: nextSlideRect.left - scrollContainerRect.left + scrollContainer.scrollLeft,
top: nextSlideRect.top - scrollContainerRect.top + scrollContainer.scrollTop,
behavior: prefersReducedMotion() ? 'auto' : behavior
left: nextLeft + scrollContainer.scrollLeft,
top: nextTop + scrollContainer.scrollTop,
behavior
});
}
render() {
const { scrollController, slidesPerPage } = this;
const { slidesPerMove, scrolling } = this;
const pagesCount = this.getPageCount();
const currentPage = this.getCurrentPage();
const prevEnabled = this.loop || currentPage > 0;
const nextEnabled = this.loop || currentPage < pagesCount - 1;
const isLtr = this.localize.dir() === 'ltr';
const prevEnabled = this.canScrollPrev();
const nextEnabled = this.canScrollNext();
const isLtr = this.matches(':dir(ltr)');
return html`
<div part="base" class="carousel">
@ -392,13 +498,16 @@ export default class SlCarousel extends ShoelaceElement {
class="${classMap({
carousel__slides: true,
'carousel__slides--horizontal': this.orientation === 'horizontal',
'carousel__slides--vertical': this.orientation === 'vertical'
'carousel__slides--vertical': this.orientation === 'vertical',
'carousel__slides--dragging': this.dragging
})}"
style="--slides-per-page: ${this.slidesPerPage};"
aria-busy="${scrollController.scrolling ? 'true' : 'false'}"
aria-busy="${scrolling ? 'true' : 'false'}"
aria-atomic="true"
tabindex="0"
@keydown=${this.handleKeyDown}
@mousedown="${this.handleMouseDragStart}"
@scroll="${this.handleScroll}"
@scrollend=${this.handleScrollEnd}
>
<slot></slot>
@ -459,7 +568,7 @@ export default class SlCarousel extends ShoelaceElement {
aria-selected="${isActive ? 'true' : 'false'}"
aria-label="${this.localize.term('goToSlide', index + 1, pagesCount)}"
tabindex=${isActive ? '0' : '-1'}
@click=${() => this.goToSlide(index * slidesPerPage)}
@click=${() => this.goToSlide(index * slidesPerMove)}
@keydown=${this.handleKeyDown}
></button>
`;

Wyświetl plik

@ -1,9 +1,6 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles.js';
export default css`
${componentStyles}
:host {
--slide-gap: var(--sl-spacing-medium, 1rem);
--aspect-ratio: 16 / 9;
@ -79,9 +76,7 @@ export default css`
overflow-x: hidden;
}
.carousel__slides--dragging,
.carousel__slides--dropping {
scroll-snap-type: unset;
.carousel__slides--dragging {
}
:host([vertical]) ::slotted(sl-carousel-item) {

Wyświetl plik

@ -1,10 +1,41 @@
import '../../../dist/shoelace.js';
import { clickOnElement } from '../../internal/test.js';
import { expect, fixture, html, oneEvent } from '@open-wc/testing';
import { aTimeout, expect, fixture, html, nextFrame, oneEvent, waitUntil } from '@open-wc/testing';
import { clickOnElement, dragElement, moveMouseOnElement } from '../../internal/test.js';
import { map } from 'lit/directives/map.js';
import { range } from 'lit/directives/range.js';
import { resetMouse } from '@web/test-runner-commands';
import sinon from 'sinon';
import type { SinonStub } from 'sinon';
import type SlCarousel from './carousel.js';
describe('<sl-carousel>', () => {
const sandbox = sinon.createSandbox();
const ioCallbacks = new Map<IntersectionObserver, SinonStub>();
const intersectionObserverCallbacks = () => {
const callbacks = [...ioCallbacks.values()];
return waitUntil(() => callbacks.every(callback => callback.called));
};
const OriginalIntersectionObserver = globalThis.IntersectionObserver;
beforeEach(() => {
globalThis.IntersectionObserver = class IntersectionObserverMock extends OriginalIntersectionObserver {
constructor(callback: IntersectionObserverCallback, options?: IntersectionObserverInit) {
const stubCallback = sandbox.stub().callsFake(callback);
super(stubCallback, options);
ioCallbacks.set(this, stubCallback);
}
};
});
afterEach(async () => {
await resetMouse();
sandbox.restore();
globalThis.IntersectionObserver = OriginalIntersectionObserver;
ioCallbacks.clear();
});
it('should render a carousel with default configuration', async () => {
// Arrange
const el = await fixture(html`
@ -27,15 +58,11 @@ describe('<sl-carousel>', () => {
let clock: sinon.SinonFakeTimers;
beforeEach(() => {
clock = sinon.useFakeTimers({
clock = sandbox.useFakeTimers({
now: new Date()
});
});
afterEach(() => {
clock.restore();
});
it('should scroll forwards every `autoplay-interval` milliseconds', async () => {
// Arrange
const el = await fixture<SlCarousel>(html`
@ -45,7 +72,7 @@ describe('<sl-carousel>', () => {
<sl-carousel-item>Node 3</sl-carousel-item>
</sl-carousel>
`);
sinon.stub(el, 'next');
sandbox.stub(el, 'next');
await el.updateComplete;
@ -66,7 +93,7 @@ describe('<sl-carousel>', () => {
<sl-carousel-item>Node 3</sl-carousel-item>
</sl-carousel>
`);
sinon.stub(el, 'next');
sandbox.stub(el, 'next');
await el.updateComplete;
@ -89,7 +116,7 @@ describe('<sl-carousel>', () => {
<sl-carousel-item>Node 3</sl-carousel-item>
</sl-carousel>
`);
sinon.stub(el, 'next');
sandbox.stub(el, 'next');
await el.updateComplete;
@ -176,7 +203,7 @@ describe('<sl-carousel>', () => {
<sl-carousel-item>Node 3</sl-carousel-item>
</sl-carousel>
`);
sinon.stub(el, 'goToSlide');
sandbox.stub(el, 'goToSlide');
await el.updateComplete;
// Act
@ -223,6 +250,36 @@ describe('<sl-carousel>', () => {
// Assert
expect(el.scrollContainer.style.getPropertyValue('--slides-per-page').trim()).to.be.equal('2');
});
[
[7, 2, 1, false, 6],
[5, 3, 3, false, 2],
[10, 2, 2, false, 5],
[7, 2, 1, true, 7],
[5, 3, 3, true, 2],
[10, 2, 2, true, 5]
].forEach(([slides, slidesPerPage, slidesPerMove, loop, expected]: [number, number, number, boolean, number]) => {
it(`should display ${expected} pages for ${slides} slides grouped by ${slidesPerPage} and scrolled by ${slidesPerMove}${
loop ? ' (loop)' : ''
}`, async () => {
// Arrange
const el = await fixture<SlCarousel>(html`
<sl-carousel
pagination
navigation
slides-per-page="${slidesPerPage}"
slides-per-move="${slidesPerMove}"
?loop=${loop}
>
${map(range(slides), i => html`<sl-carousel-item>${i}</sl-carousel-item>`)}
</sl-carousel>
`);
// Assert
const paginationItems = el.shadowRoot!.querySelectorAll('.carousel__pagination-item');
expect(paginationItems.length).to.equal(expected);
});
});
});
describe('when `slides-per-move` attribute is provided', () => {
@ -230,7 +287,7 @@ describe('<sl-carousel>', () => {
// Arrange
const expectedSnapGranularity = 2;
const el = await fixture<SlCarousel>(html`
<sl-carousel slides-per-move="${expectedSnapGranularity}">
<sl-carousel slides-per-page="${expectedSnapGranularity}" slides-per-move="${expectedSnapGranularity}">
<sl-carousel-item>Node 1</sl-carousel-item>
<sl-carousel-item>Node 2</sl-carousel-item>
<sl-carousel-item>Node 3</sl-carousel-item>
@ -252,6 +309,96 @@ describe('<sl-carousel>', () => {
}
}
});
it('should be possible to move by the given number of slides at a time', async () => {
// Arrange
const el = await fixture<SlCarousel>(html`
<sl-carousel navigation slides-per-move="2" slides-per-page="2">
<sl-carousel-item>Node 1</sl-carousel-item>
<sl-carousel-item>Node 2</sl-carousel-item>
<sl-carousel-item class="expected">Node 3</sl-carousel-item>
<sl-carousel-item class="expected">Node 4</sl-carousel-item>
<sl-carousel-item>Node 5</sl-carousel-item>
<sl-carousel-item>Node 6</sl-carousel-item>
</sl-carousel>
`);
const expectedSlides = el.querySelectorAll('.expected')!;
const nextButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button--next')!;
// Act
await clickOnElement(nextButton);
await oneEvent(el.scrollContainer, 'scrollend');
await intersectionObserverCallbacks();
await el.updateComplete;
// Assert
for (const expectedSlide of expectedSlides) {
expect(expectedSlide).to.have.class('--in-view');
expect(expectedSlide).to.be.visible;
}
});
it('should be possible to move by a number that is less than the displayed number', async () => {
// Arrange
const el = await fixture<SlCarousel>(html`
<sl-carousel navigation slides-per-move="1" slides-per-page="2">
<sl-carousel-item>Node 1</sl-carousel-item>
<sl-carousel-item>Node 2</sl-carousel-item>
<sl-carousel-item>Node 3</sl-carousel-item>
<sl-carousel-item>Node 4</sl-carousel-item>
<sl-carousel-item class="expected">Node 5</sl-carousel-item>
<sl-carousel-item class="expected">Node 6</sl-carousel-item>
</sl-carousel>
`);
const expectedSlides = el.querySelectorAll('.expected')!;
const nextButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button--next')!;
// Act
await clickOnElement(nextButton);
await aTimeout(50);
await clickOnElement(nextButton);
await aTimeout(50);
await clickOnElement(nextButton);
await aTimeout(50);
await clickOnElement(nextButton);
await aTimeout(50);
await clickOnElement(nextButton);
await aTimeout(50);
await clickOnElement(nextButton);
await oneEvent(el.scrollContainer, 'scrollend');
await intersectionObserverCallbacks();
await el.updateComplete;
// Assert
for (const expectedSlide of expectedSlides) {
expect(expectedSlide).to.have.class('--in-view');
expect(expectedSlide).to.be.visible;
}
});
it('should not be possible to move by a number that is greater than the displayed number', async () => {
// Arrange
const expectedSlidesPerMove = 2;
const el = await fixture<SlCarousel>(html`
<sl-carousel slides-per-page="${expectedSlidesPerMove}">
<sl-carousel-item>Node 1</sl-carousel-item>
<sl-carousel-item>Node 2</sl-carousel-item>
<sl-carousel-item>Node 3</sl-carousel-item>
<sl-carousel-item>Node 4</sl-carousel-item>
<sl-carousel-item>Node 5</sl-carousel-item>
<sl-carousel-item>Node 6</sl-carousel-item>
</sl-carousel>
`);
// Act
el.slidesPerMove = 3;
await el.updateComplete;
// Assert
expect(el.slidesPerMove).to.be.equal(expectedSlidesPerMove);
});
});
describe('when `orientation` attribute is provided', () => {
@ -294,6 +441,53 @@ describe('<sl-carousel>', () => {
});
});
describe('when `mouse-dragging` attribute is provided', () => {
// TODO(alenaksu): skipping because failing in webkit, PointerEvent.movementX and PointerEvent.movementY seem to return incorrect values
it.skip('should be possible to drag the carousel using the mouse', async () => {
// Arrange
const el = await fixture<SlCarousel>(html`
<sl-carousel mouse-dragging>
<sl-carousel-item>Node 1</sl-carousel-item>
<sl-carousel-item>Node 2</sl-carousel-item>
<sl-carousel-item>Node 3</sl-carousel-item>
</sl-carousel>
`);
// Act
await dragElement(el, -Math.round(el.offsetWidth * 0.75));
await oneEvent(el.scrollContainer, 'scrollend');
await dragElement(el, -Math.round(el.offsetWidth * 0.75));
await oneEvent(el.scrollContainer, 'scrollend');
await el.updateComplete;
// Assert
expect(el.activeSlide).to.be.equal(2);
});
it('should be possible to interact with clickable elements', async () => {
// Arrange
const el = await fixture<SlCarousel>(html`
<sl-carousel mouse-dragging>
<sl-carousel-item><button>click me</button></sl-carousel-item>
<sl-carousel-item>Node 2</sl-carousel-item>
<sl-carousel-item>Node 3</sl-carousel-item>
</sl-carousel>
`);
const button = el.querySelector('button')!;
const clickSpy = sinon.spy();
button.addEventListener('click', clickSpy);
// Act
await moveMouseOnElement(button);
await clickOnElement(button);
// Assert
expect(clickSpy).to.have.been.called;
});
});
describe('Navigation controls', () => {
describe('when the user clicks the next button', () => {
it('should scroll to the next slide', async () => {
@ -306,7 +500,7 @@ describe('<sl-carousel>', () => {
</sl-carousel>
`);
const nextButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button--next')!;
sinon.stub(el, 'next');
sandbox.stub(el, 'next');
await el.updateComplete;
@ -329,10 +523,11 @@ describe('<sl-carousel>', () => {
</sl-carousel>
`);
const nextButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button--next')!;
sinon.stub(el, 'next');
sandbox.stub(el, 'next');
el.goToSlide(2, 'auto');
await oneEvent(el.scrollContainer, 'scrollend');
await intersectionObserverCallbacks();
await el.updateComplete;
// Act
@ -368,6 +563,9 @@ describe('<sl-carousel>', () => {
// wait scroll to actual item
await oneEvent(el.scrollContainer, 'scrollend');
await intersectionObserverCallbacks();
await el.updateComplete;
// Assert
expect(nextButton).to.have.attribute('aria-disabled', 'false');
expect(el.activeSlide).to.be.equal(0);
@ -393,7 +591,7 @@ describe('<sl-carousel>', () => {
await el.updateComplete;
const previousButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button--previous')!;
sinon.stub(el, 'previous');
sandbox.stub(el, 'previous');
await el.updateComplete;
@ -417,7 +615,7 @@ describe('<sl-carousel>', () => {
`);
const previousButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button--previous')!;
sinon.stub(el, 'previous');
sandbox.stub(el, 'previous');
await el.updateComplete;
// Act
@ -451,6 +649,8 @@ describe('<sl-carousel>', () => {
// wait scroll to actual item
await oneEvent(el.scrollContainer, 'scrollend');
await intersectionObserverCallbacks();
// Assert
expect(previousButton).to.have.attribute('aria-disabled', 'false');
expect(el.activeSlide).to.be.equal(2);
@ -465,19 +665,27 @@ describe('<sl-carousel>', () => {
it('should scroll the carousel to the next slide', async () => {
// Arrange
const el = await fixture<SlCarousel>(html`
<sl-carousel slides-per-move="2">
<sl-carousel>
<sl-carousel-item>Node 1</sl-carousel-item>
<sl-carousel-item>Node 2</sl-carousel-item>
<sl-carousel-item>Node 3</sl-carousel-item>
</sl-carousel>
`);
sinon.stub(el, 'goToSlide');
await el.updateComplete;
sandbox.spy(el, 'goToSlide');
const expectedCarouselItem: HTMLElement = el.querySelector('sl-carousel-item:nth-child(2)')!;
// Act
el.next();
await oneEvent(el.scrollContainer, 'scrollend');
await el.updateComplete;
expect(el.goToSlide).to.have.been.calledWith(2);
const containerRect = el.scrollContainer.getBoundingClientRect();
const itemRect = expectedCarouselItem.getBoundingClientRect();
// Assert
expect(el.goToSlide).to.have.been.calledWith(1);
expect(itemRect.top).to.be.equal(containerRect.top);
expect(itemRect.left).to.be.equal(containerRect.left);
});
});
@ -485,19 +693,34 @@ describe('<sl-carousel>', () => {
it('should scroll the carousel to the previous slide', async () => {
// Arrange
const el = await fixture<SlCarousel>(html`
<sl-carousel slides-per-move="2">
<sl-carousel>
<sl-carousel-item>Node 1</sl-carousel-item>
<sl-carousel-item>Node 2</sl-carousel-item>
<sl-carousel-item>Node 3</sl-carousel-item>
</sl-carousel>
`);
sinon.stub(el, 'goToSlide');
await el.updateComplete;
const expectedCarouselItem: HTMLElement = el.querySelector('sl-carousel-item:nth-child(1)')!;
el.goToSlide(1);
await oneEvent(el.scrollContainer, 'scrollend');
await intersectionObserverCallbacks();
await nextFrame();
sandbox.spy(el, 'goToSlide');
// Act
el.previous();
await oneEvent(el.scrollContainer, 'scrollend');
await intersectionObserverCallbacks();
expect(el.goToSlide).to.have.been.calledWith(-2);
const containerRect = el.scrollContainer.getBoundingClientRect();
const itemRect = expectedCarouselItem.getBoundingClientRect();
// Assert
expect(el.goToSlide).to.have.been.calledWith(0);
expect(itemRect.top).to.be.equal(containerRect.top);
expect(itemRect.left).to.be.equal(containerRect.left);
});
});
@ -516,6 +739,7 @@ describe('<sl-carousel>', () => {
// Act
el.goToSlide(2);
await oneEvent(el.scrollContainer, 'scrollend');
await intersectionObserverCallbacks();
await el.updateComplete;
// Assert

Wyświetl plik

@ -1,169 +0,0 @@
import { debounce } from '../../internal/debounce.js';
import { prefersReducedMotion } from '../../internal/animate.js';
import { waitForEvent } from '../../internal/event.js';
import type { ReactiveController, ReactiveElement } from 'lit';
interface ScrollHost extends ReactiveElement {
scrollContainer: HTMLElement;
}
/**
* A controller for handling scrolling and mouse dragging.
*/
export class ScrollController<T extends ScrollHost> implements ReactiveController {
private host: T;
private pointers = new Set();
dragging = false;
scrolling = false;
mouseDragging = false;
constructor(host: T) {
this.host = host;
host.addController(this);
}
async hostConnected() {
const host = this.host;
await host.updateComplete;
const scrollContainer = host.scrollContainer;
scrollContainer.addEventListener('scroll', this.handleScroll, { passive: true });
scrollContainer.addEventListener('pointerdown', this.handlePointerDown);
scrollContainer.addEventListener('pointerup', this.handlePointerUp);
scrollContainer.addEventListener('pointercancel', this.handlePointerUp);
scrollContainer.addEventListener('touchstart', this.handleTouchStart, { passive: true });
scrollContainer.addEventListener('touchend', this.handleTouchEnd);
}
hostDisconnected(): void {
const host = this.host;
const scrollContainer = host.scrollContainer;
scrollContainer.removeEventListener('scroll', this.handleScroll);
scrollContainer.removeEventListener('pointerdown', this.handlePointerDown);
scrollContainer.removeEventListener('pointerup', this.handlePointerUp);
scrollContainer.removeEventListener('pointercancel', this.handlePointerUp);
scrollContainer.removeEventListener('touchstart', this.handleTouchStart);
scrollContainer.removeEventListener('touchend', this.handleTouchEnd);
}
handleScroll = () => {
if (!this.scrolling) {
this.scrolling = true;
this.host.requestUpdate();
}
this.handleScrollEnd();
};
@debounce(100)
handleScrollEnd() {
if (!this.pointers.size) {
// If no pointer is active in the scroll area then the scroll has ended
this.scrolling = false;
this.host.scrollContainer.dispatchEvent(
new CustomEvent('scrollend', {
bubbles: false,
cancelable: false
})
);
this.host.requestUpdate();
} else {
// otherwise let's wait a bit more
this.handleScrollEnd();
}
}
handlePointerDown = (event: PointerEvent) => {
if (event.pointerType === 'touch') {
return;
}
this.pointers.add(event.pointerId);
const canDrag = this.mouseDragging && !this.dragging && event.button === 0;
if (canDrag) {
event.preventDefault();
this.host.scrollContainer.addEventListener('pointermove', this.handlePointerMove);
}
};
handlePointerMove = (event: PointerEvent) => {
const scrollContainer = this.host.scrollContainer;
const hasMoved = !!event.movementX || !!event.movementY;
if (!this.dragging && hasMoved) {
// Start dragging if it hasn't yet
scrollContainer.setPointerCapture(event.pointerId);
this.handleDragStart();
} else if (scrollContainer.hasPointerCapture(event.pointerId)) {
// Ignore pointers that we are not tracking
this.handleDrag(event);
}
};
handlePointerUp = (event: PointerEvent) => {
this.pointers.delete(event.pointerId);
this.host.scrollContainer.releasePointerCapture(event.pointerId);
if (this.pointers.size === 0) {
this.handleDragEnd();
}
};
handleTouchEnd = (event: TouchEvent) => {
for (const touch of event.changedTouches) {
this.pointers.delete(touch.identifier);
}
};
handleTouchStart = (event: TouchEvent) => {
for (const touch of event.touches) {
this.pointers.add(touch.identifier);
}
};
handleDragStart() {
const host = this.host;
this.dragging = true;
host.scrollContainer.style.setProperty('scroll-snap-type', 'unset');
host.requestUpdate();
}
handleDrag(event: PointerEvent) {
this.host.scrollContainer.scrollBy({
left: -event.movementX,
top: -event.movementY
});
}
async handleDragEnd() {
const host = this.host;
const scrollContainer = host.scrollContainer;
scrollContainer.removeEventListener('pointermove', this.handlePointerMove);
this.dragging = false;
const startLeft = scrollContainer.scrollLeft;
const startTop = scrollContainer.scrollTop;
scrollContainer.style.removeProperty('scroll-snap-type');
const finalLeft = scrollContainer.scrollLeft;
const finalTop = scrollContainer.scrollTop;
scrollContainer.style.setProperty('scroll-snap-type', 'unset');
scrollContainer.scrollTo({ left: startLeft, top: startTop, behavior: 'auto' });
scrollContainer.scrollTo({ left: finalLeft, top: finalTop, behavior: prefersReducedMotion() ? 'auto' : 'smooth' });
if (this.scrolling) {
await waitForEvent(scrollContainer, 'scrollend');
}
scrollContainer.style.removeProperty('scroll-snap-type');
host.requestUpdate();
}
}

Wyświetl plik

@ -1,11 +1,14 @@
import { classMap } from 'lit/directives/class-map.js';
import { defaultValue } from '../../internal/default-value.js';
import { FormControlController } from '../../internal/form.js';
import { HasSlotController } from '../../internal/slot.js';
import { html } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js';
import { live } from 'lit/directives/live.js';
import { property, query, state } from 'lit/decorators.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import formControlStyles from '../../styles/form-control.styles.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import SlIcon from '../icon/icon.component.js';
import styles from './checkbox.styles.js';
@ -21,6 +24,7 @@ import type { ShoelaceFormControl } from '../../internal/shoelace-element.js';
* @dependency sl-icon
*
* @slot - The checkbox's label.
* @slot help-text - Text that describes how to use the checkbox. Alternatively, you can use the `help-text` attribute.
*
* @event sl-blur - Emitted when the checkbox loses focus.
* @event sl-change - Emitted when the checked state changes.
@ -35,9 +39,10 @@ import type { ShoelaceFormControl } from '../../internal/shoelace-element.js';
* @csspart checked-icon - The checked icon, an `<sl-icon>` element.
* @csspart indeterminate-icon - The indeterminate icon, an `<sl-icon>` element.
* @csspart label - The container that wraps the checkbox's label.
* @csspart form-control-help-text - The help text's wrapper.
*/
export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormControl {
static styles: CSSResultGroup = styles;
static styles: CSSResultGroup = [componentStyles, formControlStyles, styles];
static dependencies = { 'sl-icon': SlIcon };
private readonly formControlController = new FormControlController(this, {
@ -45,6 +50,7 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC
defaultValue: (control: SlCheckbox) => control.defaultChecked,
setValue: (control: SlCheckbox, checked: boolean) => (control.checked = checked)
});
private readonly hasSlotController = new HasSlotController(this, 'help-text');
@query('input[type="checkbox"]') input: HTMLInputElement;
@ -86,6 +92,9 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC
/** Makes the checkbox a required field. */
@property({ type: Boolean, reflect: true }) required = false;
/** The checkbox's help text. If you need to display HTML, use the `help-text` slot instead. */
@property({ attribute: 'help-text' }) helpText = '';
/** Gets the validity state object */
get validity() {
return this.input.validity;
@ -178,68 +187,93 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC
}
render() {
const hasHelpTextSlot = this.hasSlotController.test('help-text');
const hasHelpText = this.helpText ? true : !!hasHelpTextSlot;
//
// NOTE: we use a <div> around the label slot because of this Chrome bug.
//
// https://bugs.chromium.org/p/chromium/issues/detail?id=1413733
//
return html`
<label
part="base"
<div
class=${classMap({
checkbox: true,
'checkbox--checked': this.checked,
'checkbox--disabled': this.disabled,
'checkbox--focused': this.hasFocus,
'checkbox--indeterminate': this.indeterminate,
'checkbox--small': this.size === 'small',
'checkbox--medium': this.size === 'medium',
'checkbox--large': this.size === 'large'
'form-control': true,
'form-control--small': this.size === 'small',
'form-control--medium': this.size === 'medium',
'form-control--large': this.size === 'large',
'form-control--has-help-text': hasHelpText
})}
>
<input
class="checkbox__input"
type="checkbox"
title=${this.title /* An empty title prevents browser validation tooltips from appearing on hover */}
name=${this.name}
value=${ifDefined(this.value)}
.indeterminate=${live(this.indeterminate)}
.checked=${live(this.checked)}
.disabled=${this.disabled}
.required=${this.required}
aria-checked=${this.checked ? 'true' : 'false'}
@click=${this.handleClick}
@input=${this.handleInput}
@invalid=${this.handleInvalid}
@blur=${this.handleBlur}
@focus=${this.handleFocus}
/>
<span
part="control${this.checked ? ' control--checked' : ''}${this.indeterminate ? ' control--indeterminate' : ''}"
class="checkbox__control"
<label
part="base"
class=${classMap({
checkbox: true,
'checkbox--checked': this.checked,
'checkbox--disabled': this.disabled,
'checkbox--focused': this.hasFocus,
'checkbox--indeterminate': this.indeterminate,
'checkbox--small': this.size === 'small',
'checkbox--medium': this.size === 'medium',
'checkbox--large': this.size === 'large'
})}
>
${this.checked
? html`
<sl-icon part="checked-icon" class="checkbox__checked-icon" library="system" name="check"></sl-icon>
`
: ''}
${!this.checked && this.indeterminate
? html`
<sl-icon
part="indeterminate-icon"
class="checkbox__indeterminate-icon"
library="system"
name="indeterminate"
></sl-icon>
`
: ''}
</span>
<input
class="checkbox__input"
type="checkbox"
title=${this.title /* An empty title prevents browser validation tooltips from appearing on hover */}
name=${this.name}
value=${ifDefined(this.value)}
.indeterminate=${live(this.indeterminate)}
.checked=${live(this.checked)}
.disabled=${this.disabled}
.required=${this.required}
aria-checked=${this.checked ? 'true' : 'false'}
aria-describedby="help-text"
@click=${this.handleClick}
@input=${this.handleInput}
@invalid=${this.handleInvalid}
@blur=${this.handleBlur}
@focus=${this.handleFocus}
/>
<div part="label" class="checkbox__label">
<slot></slot>
<span
part="control${this.checked ? ' control--checked' : ''}${this.indeterminate
? ' control--indeterminate'
: ''}"
class="checkbox__control"
>
${this.checked
? html`
<sl-icon part="checked-icon" class="checkbox__checked-icon" library="system" name="check"></sl-icon>
`
: ''}
${!this.checked && this.indeterminate
? html`
<sl-icon
part="indeterminate-icon"
class="checkbox__indeterminate-icon"
library="system"
name="indeterminate"
></sl-icon>
`
: ''}
</span>
<div part="label" class="checkbox__label">
<slot></slot>
</div>
</label>
<div
aria-hidden=${hasHelpText ? 'false' : 'true'}
class="form-control__help-text"
id="help-text"
part="form-control-help-text"
>
<slot name="help-text">${this.helpText}</slot>
</div>
</label>
</div>
`;
}
}

Wyświetl plik

@ -1,9 +1,6 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles.js';
export default css`
${componentStyles}
:host {
display: inline-block;
}
@ -46,8 +43,11 @@ export default css`
border-radius: 2px;
background-color: var(--sl-input-background-color);
color: var(--sl-color-neutral-0);
transition: var(--sl-transition-fast) border-color, var(--sl-transition-fast) background-color,
var(--sl-transition-fast) color, var(--sl-transition-fast) box-shadow;
transition:
var(--sl-transition-fast) border-color,
var(--sl-transition-fast) background-color,
var(--sl-transition-fast) color,
var(--sl-transition-fast) box-shadow;
}
.checkbox__input {
@ -110,10 +110,12 @@ export default css`
line-height: var(--toggle-size);
margin-inline-start: 0.5em;
user-select: none;
-webkit-user-select: none;
}
:host([required]) .checkbox__label::after {
content: var(--sl-input-required-content);
color: var(--sl-input-required-content-color);
margin-inline-start: var(--sl-input-required-content-offset);
}
`;

Wyświetl plik

@ -23,6 +23,7 @@ describe('<sl-checkbox>', () => {
expect(el.checked).to.be.false;
expect(el.indeterminate).to.be.false;
expect(el.defaultChecked).to.be.false;
expect(el.helpText).to.equal('');
});
it('should have title if title attribute is set', async () => {

Wyświetl plik

@ -2,14 +2,15 @@ import { clamp } from '../../internal/math.js';
import { classMap } from 'lit/directives/class-map.js';
import { defaultValue } from '../../internal/default-value.js';
import { drag } from '../../internal/drag.js';
import { eventOptions, property, query, state } from 'lit/decorators.js';
import { FormControlController } from '../../internal/form.js';
import { html } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js';
import { LocalizeController } from '../../utilities/localize.js';
import { property, query, state } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js';
import { TinyColor } from '@ctrl/tinycolor';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import SlButton from '../button/button.component.js';
import SlButtonGroup from '../button-group/button-group.component.js';
@ -90,7 +91,7 @@ declare const EyeDropper: EyeDropperConstructor;
* @cssproperty --swatch-size - The size of each predefined color swatch.
*/
export default class SlColorPicker extends ShoelaceElement implements ShoelaceFormControl {
static styles: CSSResultGroup = styles;
static styles: CSSResultGroup = [componentStyles, styles];
static dependencies = {
'sl-button-group': SlButtonGroup,
@ -243,7 +244,8 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
const container = this.shadowRoot!.querySelector<HTMLElement>('.color-picker__slider.color-picker__alpha')!;
const handle = container.querySelector<HTMLElement>('.color-picker__slider-handle')!;
const { width } = container.getBoundingClientRect();
let oldValue = this.value;
let initialValue = this.value;
let currentValue = this.value;
handle.focus();
event.preventDefault();
@ -253,12 +255,17 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
this.alpha = clamp((x / width) * 100, 0, 100);
this.syncValues();
if (this.value !== oldValue) {
oldValue = this.value;
this.emit('sl-change');
if (this.value !== currentValue) {
currentValue = this.value;
this.emit('sl-input');
}
},
onStop: () => {
if (this.value !== initialValue) {
initialValue = this.value;
this.emit('sl-change');
}
},
initialEvent: event
});
}
@ -267,7 +274,8 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
const container = this.shadowRoot!.querySelector<HTMLElement>('.color-picker__slider.color-picker__hue')!;
const handle = container.querySelector<HTMLElement>('.color-picker__slider-handle')!;
const { width } = container.getBoundingClientRect();
let oldValue = this.value;
let initialValue = this.value;
let currentValue = this.value;
handle.focus();
event.preventDefault();
@ -277,12 +285,17 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
this.hue = clamp((x / width) * 360, 0, 360);
this.syncValues();
if (this.value !== oldValue) {
oldValue = this.value;
this.emit('sl-change');
if (this.value !== currentValue) {
currentValue = this.value;
this.emit('sl-input');
}
},
onStop: () => {
if (this.value !== initialValue) {
initialValue = this.value;
this.emit('sl-change');
}
},
initialEvent: event
});
}
@ -291,7 +304,8 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
const grid = this.shadowRoot!.querySelector<HTMLElement>('.color-picker__grid')!;
const handle = grid.querySelector<HTMLElement>('.color-picker__grid-handle')!;
const { width, height } = grid.getBoundingClientRect();
let oldValue = this.value;
let initialValue = this.value;
let currentValue = this.value;
handle.focus();
event.preventDefault();
@ -304,13 +318,18 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
this.brightness = clamp(100 - (y / height) * 100, 0, 100);
this.syncValues();
if (this.value !== oldValue) {
oldValue = this.value;
this.emit('sl-change');
if (this.value !== currentValue) {
currentValue = this.value;
this.emit('sl-input');
}
},
onStop: () => (this.isDraggingGridHandle = false),
onStop: () => {
this.isDraggingGridHandle = false;
if (this.value !== initialValue) {
initialValue = this.value;
this.emit('sl-change');
}
},
initialEvent: event
});
}
@ -469,6 +488,7 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
this.formControlController.emitInvalidEvent(event);
}
@eventOptions({ passive: false })
private handleTouchMove(event: TouchEvent) {
event.preventDefault();
}
@ -649,7 +669,7 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
/** Generates a hex string from HSV values. Hue must be 0-360. All other arguments must be 0-100. */
private getHexString(hue: number, saturation: number, brightness: number, alpha = 100) {
const color = new TinyColor(`hsva(${hue}, ${saturation}, ${brightness}, ${alpha / 100})`);
const color = new TinyColor(`hsva(${hue}, ${saturation}%, ${brightness}%, ${alpha / 100})`);
if (!color.isValid) {
return '';
}

Wyświetl plik

@ -1,9 +1,6 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles.js';
export default css`
${componentStyles}
:host {
--grid-width: 280px;
--grid-height: 200px;
@ -24,6 +21,7 @@ export default css`
background-color: var(--sl-panel-background-color);
border-radius: var(--sl-border-radius-medium);
user-select: none;
-webkit-user-select: none;
}
.color-picker--inline {
@ -245,7 +243,11 @@ export default css`
linear-gradient(45deg, transparent 75%, var(--sl-color-neutral-300) 75%),
linear-gradient(45deg, var(--sl-color-neutral-300) 25%, transparent 25%);
background-size: 10px 10px;
background-position: 0 0, 0 0, -5px -5px, 5px 5px;
background-position:
0 0,
0 0,
-5px -5px,
5px 5px;
}
.color-picker--disabled {
@ -311,7 +313,9 @@ export default css`
height: 100%;
border-radius: inherit;
background-color: currentColor;
box-shadow: inset 0 0 0 2px var(--sl-input-border-color), inset 0 0 0 4px var(--sl-color-neutral-0);
box-shadow:
inset 0 0 0 2px var(--sl-input-border-color),
inset 0 0 0 4px var(--sl-color-neutral-0);
}
.color-dropdown__trigger--empty:before {

Wyświetl plik

@ -1,6 +1,6 @@
import '../../../dist/shoelace.js';
import { aTimeout, expect, fixture, html, oneEvent } from '@open-wc/testing';
import { clickOnElement } from '../../internal/test.js';
import { clickOnElement, dragElement } from '../../internal/test.js';
import { runFormControlBaseTests } from '../../internal/test/form-control-base-tests.js';
import { sendKeys } from '@web/test-runner-commands';
import { serialize } from '../../utilities/form.js';
@ -31,11 +31,22 @@ describe('<sl-color-picker>', () => {
await clickOnElement(trigger); // open the dropdown
await aTimeout(200); // wait for the dropdown to open
await clickOnElement(grid); // click on the grid
// Simulate a drag event. "sl-change" should not fire until we stop dragging.
await dragElement(grid, 2, 0, {
afterMouseDown: () => {
expect(changeHandler).to.have.not.been.called;
expect(inputHandler).to.have.been.calledOnce;
},
afterMouseMove: () => {
expect(inputHandler).to.have.been.calledTwice;
}
});
await el.updateComplete;
expect(changeHandler).to.have.been.calledOnce;
expect(inputHandler).to.have.been.calledOnce;
expect(inputHandler).to.have.been.calledTwice;
});
it('should emit sl-change and sl-input when the hue slider is moved', async () => {
@ -50,10 +61,22 @@ describe('<sl-color-picker>', () => {
await clickOnElement(trigger); // open the dropdown
await aTimeout(200); // wait for the dropdown to open
await clickOnElement(slider); // click on the hue slider
// Simulate a drag event. "sl-change" should not fire until we stop dragging.
await dragElement(slider, 20, 0, {
afterMouseDown: () => {
expect(changeHandler).to.have.not.been.called;
expect(inputHandler).to.have.been.calledOnce;
},
afterMouseMove: () => {
// It's not twice because you can't change the hue of white!
expect(inputHandler).to.have.been.calledOnce;
}
});
await el.updateComplete;
expect(changeHandler).to.have.been.calledOnce;
// It's not twice because you can't change the hue of white!
expect(inputHandler).to.have.been.calledOnce;
});
@ -69,11 +92,22 @@ describe('<sl-color-picker>', () => {
await clickOnElement(trigger); // open the dropdown
await aTimeout(200); // wait for the dropdown to open
await clickOnElement(slider); // click on the opacity slider
// Simulate a drag event. "sl-change" should not fire until we stop dragging.
await dragElement(slider, 2, 0, {
afterMouseDown: () => {
expect(changeHandler).to.have.not.been.called;
expect(inputHandler).to.have.been.calledOnce;
},
afterMouseMove: () => {
expect(inputHandler).to.have.been.calledTwice;
}
});
await el.updateComplete;
expect(changeHandler).to.have.been.calledOnce;
expect(inputHandler).to.have.been.calledOnce;
expect(inputHandler).to.have.been.calledTwice;
});
it('should emit sl-change and sl-input when toggling the format', async () => {
@ -97,9 +131,9 @@ describe('<sl-color-picker>', () => {
});
it('should render the correct swatches when passing a string of color values', async () => {
const el = await fixture<SlColorPicker>(
html` <sl-color-picker swatches="red; #008000; rgb(0,0,255);"></sl-color-picker> `
);
const el = await fixture<SlColorPicker>(html`
<sl-color-picker swatches="red; #008000; rgb(0,0,255);"></sl-color-picker>
`);
const swatches = [...el.shadowRoot!.querySelectorAll('[part~="swatch"] > div')];
expect(swatches.length).to.equal(3);
@ -326,7 +360,7 @@ describe('<sl-color-picker>', () => {
expect(previewColor).to.equal('#ff000050');
});
it('should emit sl-focus when rendered as a dropdown and focused', async () => {
it.skip('should emit sl-focus when rendered as a dropdown and focused', async () => {
const el = await fixture<SlColorPicker>(html`
<div>
<sl-color-picker></sl-color-picker>

Wyświetl plik

@ -3,6 +3,7 @@ import { getAnimation, setDefaultAnimation } from '../../utilities/animation-reg
import { html } from 'lit';
import { LocalizeController } from '../../utilities/localize.js';
import { property, query, state } from 'lit/decorators.js';
import componentStyles from '../../styles/component.styles.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import SlIcon from '../icon/icon.component.js';
import SlTooltip from '../tooltip/tooltip.component.js';
@ -41,7 +42,7 @@ import type { CSSResultGroup } from 'lit';
* @animation copy.out - The animation to use when feedback icons animate out.
*/
export default class SlCopyButton extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static styles: CSSResultGroup = [componentStyles, styles];
static dependencies = {
'sl-icon': SlIcon,
'sl-tooltip': SlTooltip
@ -206,9 +207,9 @@ export default class SlCopyButton extends ShoelaceElement {
?disabled=${this.disabled}
?hoist=${this.hoist}
exportparts="
base:tooltip__base
base__popup:tooltip__base__popup
base__arrow:tooltip__base__arrow
base:tooltip__base,
base__popup:tooltip__base__popup,
base__arrow:tooltip__base__arrow,
body:tooltip__body
"
>

Wyświetl plik

@ -1,9 +1,6 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles.js';
export default css`
${componentStyles}
:host {
--error-color: var(--sl-color-danger-600);
--success-color: var(--sl-color-success-600);

Wyświetl plik

@ -6,6 +6,7 @@ import { LocalizeController } from '../../utilities/localize.js';
import { property, query } from 'lit/decorators.js';
import { waitForEvent } from '../../internal/event.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import SlIcon from '../icon/icon.component.js';
import styles from './details.styles.js';
@ -39,7 +40,7 @@ import type { CSSResultGroup } from 'lit';
* @animation details.hide - The animation to use when hiding details. You can use `height: auto` with this animation.
*/
export default class SlDetails extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static styles: CSSResultGroup = [componentStyles, styles];
static dependencies = {
'sl-icon': SlIcon
@ -87,6 +88,7 @@ export default class SlDetails extends ShoelaceElement {
}
disconnectedCallback() {
super.disconnectedCallback();
this.detailsObserver.disconnect();
}
@ -185,7 +187,7 @@ export default class SlDetails extends ShoelaceElement {
}
render() {
const isRtl = this.localize.dir() === 'rtl';
const isRtl = this.matches(':dir(rtl)');
return html`
<details

Wyświetl plik

@ -1,9 +1,6 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles.js';
export default css`
${componentStyles}
:host {
display: block;
}
@ -25,6 +22,7 @@ export default css`
border-radius: inherit;
padding: var(--sl-spacing-medium);
user-select: none;
-webkit-user-select: none;
cursor: pointer;
}

Wyświetl plik

@ -2,9 +2,9 @@ import '../../../dist/shoelace.js';
// cspell:dictionaries lorem-ipsum
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import sinon from 'sinon';
import type { SlHideEvent } from '../../events/sl-hide';
import type { SlShowEvent } from '../../events/sl-show';
import type SlDetails from './details';
import type { SlHideEvent } from '../../events/sl-hide.js';
import type { SlShowEvent } from '../../events/sl-show.js';
import type SlDetails from './details.js';
describe('<sl-details>', () => {
describe('accessibility', () => {

Wyświetl plik

@ -9,6 +9,7 @@ import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll.js
import { property, query } from 'lit/decorators.js';
import { waitForEvent } from '../../internal/event.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import Modal from '../../internal/modal.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import SlIconButton from '../icon-button/icon-button.component.js';
@ -60,17 +61,22 @@ import type { CSSResultGroup } from 'lit';
* @animation dialog.denyClose - The animation to use when a request to close the dialog is denied.
* @animation dialog.overlay.show - The animation to use when showing the dialog's overlay.
* @animation dialog.overlay.hide - The animation to use when hiding the dialog's overlay.
*
* @property modal - Exposes the internal modal utility that controls focus trapping. To temporarily disable focus
* trapping and allow third-party modals spawned from an active Shoelace modal, call `modal.activateExternal()` when
* the third-party modal opens. Upon closing, call `modal.deactivateExternal()` to restore Shoelace's focus trapping.
*/
export default class SlDialog extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static styles: CSSResultGroup = [componentStyles, styles];
static dependencies = {
'sl-icon-button': SlIconButton
};
private readonly hasSlotController = new HasSlotController(this, 'footer');
private readonly localize = new LocalizeController(this);
private modal = new Modal(this);
private originalTrigger: HTMLElement | null;
public modal = new Modal(this);
private closeWatcher: CloseWatcher | null;
@query('.dialog') dialog: HTMLElement;
@query('.dialog__panel') panel: HTMLElement;
@ -108,6 +114,7 @@ export default class SlDialog extends ShoelaceElement {
super.disconnectedCallback();
this.modal.deactivate();
unlockBodyScrolling(this);
this.closeWatcher?.destroy();
}
private requestClose(source: 'close-button' | 'keyboard' | 'overlay') {
@ -126,10 +133,17 @@ export default class SlDialog extends ShoelaceElement {
}
private addOpenListeners() {
document.addEventListener('keydown', this.handleDocumentKeyDown);
if ('CloseWatcher' in window) {
this.closeWatcher?.destroy();
this.closeWatcher = new CloseWatcher();
this.closeWatcher.onclose = () => this.requestClose('keyboard');
} else {
document.addEventListener('keydown', this.handleDocumentKeyDown);
}
}
private removeOpenListeners() {
this.closeWatcher?.destroy();
document.removeEventListener('keydown', this.handleDocumentKeyDown);
}
@ -296,9 +310,9 @@ export default class SlDialog extends ShoelaceElement {
`
: ''}
${
'' /* The tabindex="-1" is here because the body is technically scrollable if overflowing. However, if there's no focusable elements inside, you won't actually be able to scroll it via keyboard. */
'' /* The tabindex="-1" is here because the body is technically scrollable if overflowing. However, if there's no focusable elements inside, you won't actually be able to scroll it via keyboard. Previously this was just a <slot>, but tabindex="-1" on the slot causes children to not be focusable. https://github.com/shoelace-style/shoelace/issues/1753#issuecomment-1836803277 */
}
<slot part="body" class="dialog__body" tabindex="-1"></slot>
<div part="body" class="dialog__body" tabindex="-1"><slot></slot></div>
<footer part="footer" class="dialog__footer">
<slot name="footer"></slot>

Wyświetl plik

@ -1,9 +1,6 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles.js';
export default css`
${componentStyles}
:host {
--width: 31rem;
--header-spacing: var(--sl-spacing-large);

Wyświetl plik

@ -1,10 +1,10 @@
import '../../../dist/shoelace.js';
// cspell:dictionaries lorem-ipsum
import { aTimeout, elementUpdated, expect, fixture, html, waitUntil } from '@open-wc/testing';
import { LitElement } from 'lit';
import { aTimeout, elementUpdated, expect, fixture, waitUntil } from '@open-wc/testing';
import { html, LitElement } from 'lit';
import { sendKeys } from '@web/test-runner-commands';
import sinon from 'sinon';
import type SlDialog from './dialog';
import type SlDialog from './dialog.js';
describe('<sl-dialog>', () => {
it('should be visible with the open attribute', async () => {
@ -17,9 +17,9 @@ describe('<sl-dialog>', () => {
});
it('should not be visible without the open attribute', async () => {
const el = await fixture<SlDialog>(
html` <sl-dialog>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog> `
);
const el = await fixture<SlDialog>(html`
<sl-dialog>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-dialog>
`);
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
expect(base.hidden).to.be.true;
@ -211,7 +211,7 @@ describe('<sl-dialog>', () => {
// Opens modal.
const openModalButton = container.shadowRoot?.querySelector('sl-button');
if (openModalButton) openModalButton.click();
openModalButton!.click();
// Test tab cycling
await pressTab();

Wyświetl plik

@ -1,5 +1,6 @@
import { property } from 'lit/decorators.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import styles from './divider.styles.js';
import type { CSSResultGroup } from 'lit';
@ -15,7 +16,7 @@ import type { CSSResultGroup } from 'lit';
* @cssproperty --spacing - The spacing of the divider.
*/
export default class SlDivider extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static styles: CSSResultGroup = [componentStyles, styles];
/** Draws the divider in a vertical orientation. */
@property({ type: Boolean, reflect: true }) vertical = false;

Wyświetl plik

@ -1,9 +1,6 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles.js';
export default css`
${componentStyles}
:host {
--color: var(--sl-panel-border-color);
--width: var(--sl-panel-border-width);

Wyświetl plik

@ -10,6 +10,7 @@ import { property, query } from 'lit/decorators.js';
import { uppercaseFirstLetter } from '../../internal/string.js';
import { waitForEvent } from '../../internal/event.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import Modal from '../../internal/modal.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import SlIconButton from '../icon-button/icon-button.component.js';
@ -68,15 +69,20 @@ import type { CSSResultGroup } from 'lit';
* @animation drawer.denyClose - The animation to use when a request to close the drawer is denied.
* @animation drawer.overlay.show - The animation to use when showing the drawer's overlay.
* @animation drawer.overlay.hide - The animation to use when hiding the drawer's overlay.
*
* @property modal - Exposes the internal modal utility that controls focus trapping. To temporarily disable focus
* trapping and allow third-party modals spawned from an active Shoelace modal, call `modal.activateExternal()` when
* the third-party modal opens. Upon closing, call `modal.deactivateExternal()` to restore Shoelace's focus trapping.
*/
export default class SlDrawer extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static styles: CSSResultGroup = [componentStyles, styles];
static dependencies = { 'sl-icon-button': SlIconButton };
private readonly hasSlotController = new HasSlotController(this, 'footer');
private readonly localize = new LocalizeController(this);
private modal = new Modal(this);
private originalTrigger: HTMLElement | null;
public modal = new Modal(this);
private closeWatcher: CloseWatcher | null;
@query('.drawer') drawer: HTMLElement;
@query('.drawer__panel') panel: HTMLElement;
@ -125,6 +131,7 @@ export default class SlDrawer extends ShoelaceElement {
disconnectedCallback() {
super.disconnectedCallback();
unlockBodyScrolling(this);
this.closeWatcher?.destroy();
}
private requestClose(source: 'close-button' | 'keyboard' | 'overlay') {
@ -143,11 +150,20 @@ export default class SlDrawer extends ShoelaceElement {
}
private addOpenListeners() {
document.addEventListener('keydown', this.handleDocumentKeyDown);
if ('CloseWatcher' in window) {
this.closeWatcher?.destroy();
if (!this.contained) {
this.closeWatcher = new CloseWatcher();
this.closeWatcher.onclose = () => this.requestClose('keyboard');
}
} else {
document.addEventListener('keydown', this.handleDocumentKeyDown);
}
}
private removeOpenListeners() {
document.removeEventListener('keydown', this.handleDocumentKeyDown);
this.closeWatcher?.destroy();
}
private handleDocumentKeyDown = (event: KeyboardEvent) => {

Wyświetl plik

@ -1,9 +1,6 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles.js';
export default css`
${componentStyles}
:host {
--size: 25rem;
--header-spacing: var(--sl-spacing-large);

Wyświetl plik

@ -3,7 +3,7 @@ import '../../../dist/shoelace.js';
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import { sendKeys } from '@web/test-runner-commands';
import sinon from 'sinon';
import type SlDrawer from './drawer';
import type SlDrawer from './drawer.js';
describe('<sl-drawer>', () => {
it('should be visible with the open attribute', async () => {
@ -16,9 +16,9 @@ describe('<sl-drawer>', () => {
});
it('should not be visible without the open attribute', async () => {
const el = await fixture<SlDrawer>(
html` <sl-drawer>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer> `
);
const el = await fixture<SlDrawer>(html`
<sl-drawer>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</sl-drawer>
`);
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
expect(base.hidden).to.be.true;

Wyświetl plik

@ -3,10 +3,12 @@ import { classMap } from 'lit/directives/class-map.js';
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry.js';
import { getTabbableBoundary } from '../../internal/tabbable.js';
import { html } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js';
import { LocalizeController } from '../../utilities/localize.js';
import { property, query } from 'lit/decorators.js';
import { waitForEvent } from '../../internal/event.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import SlPopup from '../popup/popup.component.js';
import styles from './dropdown.styles.js';
@ -40,7 +42,7 @@ import type SlMenu from '../menu/menu.js';
* @animation dropdown.hide - The animation to use when hiding the dropdown.
*/
export default class SlDropdown extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static styles: CSSResultGroup = [componentStyles, styles];
static dependencies = { 'sl-popup': SlPopup };
@query('.dropdown') popup: SlPopup;
@ -48,6 +50,7 @@ export default class SlDropdown extends ShoelaceElement {
@query('.dropdown__panel') panel: HTMLSlotElement;
private readonly localize = new LocalizeController(this);
private closeWatcher: CloseWatcher | null;
/**
* Indicates whether or not the dropdown is open. You can toggle this attribute to show and hide the dropdown, or you
@ -100,6 +103,11 @@ export default class SlDropdown extends ShoelaceElement {
*/
@property({ type: Boolean }) hoist = false;
/**
* Syncs the popup width or height to that of the trigger element.
*/
@property({ reflect: true }) sync: 'width' | 'height' | 'both' | undefined = undefined;
connectedCallback() {
super.connectedCallback();
@ -149,7 +157,7 @@ export default class SlDropdown extends ShoelaceElement {
private handleDocumentKeyDown = (event: KeyboardEvent) => {
// Close when escape or tab is pressed
if (event.key === 'Escape' && this.open) {
if (event.key === 'Escape' && this.open && !this.closeWatcher) {
event.stopPropagation();
this.focusOnTrigger();
this.hide();
@ -334,7 +342,16 @@ export default class SlDropdown extends ShoelaceElement {
addOpenListeners() {
this.panel.addEventListener('sl-select', this.handlePanelSelect);
this.panel.addEventListener('keydown', this.handleKeyDown);
if ('CloseWatcher' in window) {
this.closeWatcher?.destroy();
this.closeWatcher = new CloseWatcher();
this.closeWatcher.onclose = () => {
this.hide();
this.focusOnTrigger();
};
} else {
this.panel.addEventListener('keydown', this.handleKeyDown);
}
document.addEventListener('keydown', this.handleDocumentKeyDown);
document.addEventListener('mousedown', this.handleDocumentMouseDown);
}
@ -346,6 +363,7 @@ export default class SlDropdown extends ShoelaceElement {
}
document.removeEventListener('keydown', this.handleDocumentKeyDown);
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
this.closeWatcher?.destroy();
}
@watch('open', { waitUntilFirstUpdate: true })
@ -397,6 +415,7 @@ export default class SlDropdown extends ShoelaceElement {
shift
auto-size="vertical"
auto-size-padding="10"
sync=${ifDefined(this.sync ? this.sync : undefined)}
class=${classMap({
dropdown: true,
'dropdown--open': this.open

Wyświetl plik

@ -1,9 +1,6 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles.js';
export default css`
${componentStyles}
:host {
display: inline-block;
}

Wyświetl plik

@ -354,27 +354,4 @@ describe('<sl-dropdown>', () => {
expect(el.open).to.be.false;
});
it('should close and stop propagating the keydown event when Escape is pressed and the dropdown is open ', async () => {
const el = await fixture<SlDropdown>(html`
<sl-dropdown open>
<sl-button slot="trigger" caret>Toggle</sl-button>
<sl-menu>
<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>
</sl-dropdown>
`);
const firstMenuItem = el.querySelector('sl-menu-item')!;
const hideHandler = sinon.spy();
document.body.addEventListener('keydown', hideHandler);
firstMenuItem.focus();
await sendKeys({ press: 'Escape' });
await el.updateComplete;
expect(el.open).to.be.false;
expect(hideHandler).to.not.have.been.called;
});
});

Wyświetl plik

@ -1,6 +1,6 @@
import '../../../dist/shoelace.js';
import { elementUpdated, expect, fixture, html } from '@open-wc/testing';
import type SlFormatBytes from './format-bytes';
import type SlFormatBytes from './format-bytes.js';
describe('<sl-format-bytes>', () => {
describe('defaults ', () => {

Wyświetl plik

@ -1,7 +1,7 @@
import '../../../dist/shoelace.js';
import { expect, fixture, html } from '@open-wc/testing';
import sinon from 'sinon';
import type SlFormatDate from './format-date';
import type SlFormatDate from './format-date.js';
describe('<sl-format-date>', () => {
describe('defaults ', () => {
@ -52,11 +52,9 @@ describe('<sl-format-date>', () => {
];
results.forEach(setup => {
it(`date has correct language format: ${setup.lang}`, async () => {
const el = await fixture<SlFormatDate>(
html`
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" lang="${setup.lang}"></sl-format-date>
`
);
const el = await fixture<SlFormatDate>(html`
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" lang="${setup.lang}"></sl-format-date>
`);
expect(el.shadowRoot?.textContent?.trim()).to.equal(setup.result);
});
});
@ -66,14 +64,12 @@ describe('<sl-format-date>', () => {
const weekdays = ['narrow', 'short', 'long'];
weekdays.forEach((weekdayFormat: 'narrow' | 'short' | 'long') => {
it(`date has correct weekday format: ${weekdayFormat}`, async () => {
const el = await fixture<SlFormatDate>(
html`
<sl-format-date
.date="${new Date(new Date().getFullYear(), 0, 1)}"
weekday="${weekdayFormat}"
></sl-format-date>
`
);
const el = await fixture<SlFormatDate>(html`
<sl-format-date
.date="${new Date(new Date().getFullYear(), 0, 1)}"
weekday="${weekdayFormat}"
></sl-format-date>
`);
const expected = new Intl.DateTimeFormat('en-US', { weekday: weekdayFormat }).format(
new Date(new Date().getFullYear(), 0, 1)
@ -87,11 +83,9 @@ describe('<sl-format-date>', () => {
const eras = ['narrow', 'short', 'long'];
eras.forEach((eraFormat: 'narrow' | 'short' | 'long') => {
it(`date has correct era format: ${eraFormat}`, async () => {
const el = await fixture<SlFormatDate>(
html`
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" era="${eraFormat}"></sl-format-date>
`
);
const el = await fixture<SlFormatDate>(html`
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" era="${eraFormat}"></sl-format-date>
`);
const expected = new Intl.DateTimeFormat('en-US', { era: eraFormat }).format(
new Date(new Date().getFullYear(), 0, 1)
@ -105,11 +99,9 @@ describe('<sl-format-date>', () => {
const yearFormats = ['numeric', '2-digit'];
yearFormats.forEach((yearFormat: 'numeric' | '2-digit') => {
it(`date has correct year format: ${yearFormat}`, async () => {
const el = await fixture<SlFormatDate>(
html`
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" year="${yearFormat}"></sl-format-date>
`
);
const el = await fixture<SlFormatDate>(html`
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" year="${yearFormat}"></sl-format-date>
`);
const expected = new Intl.DateTimeFormat('en-US', { year: yearFormat }).format(
new Date(new Date().getFullYear(), 0, 1)
@ -123,11 +115,9 @@ describe('<sl-format-date>', () => {
const monthFormats = ['numeric', '2-digit', 'narrow', 'short', 'long'];
monthFormats.forEach((monthFormat: 'numeric' | '2-digit' | 'narrow' | 'short' | 'long') => {
it(`date has correct month format: ${monthFormat}`, async () => {
const el = await fixture<SlFormatDate>(
html`
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" month="${monthFormat}"></sl-format-date>
`
);
const el = await fixture<SlFormatDate>(html`
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" month="${monthFormat}"></sl-format-date>
`);
const expected = new Intl.DateTimeFormat('en-US', { month: monthFormat }).format(
new Date(new Date().getFullYear(), 0, 1)
@ -141,11 +131,9 @@ describe('<sl-format-date>', () => {
const dayFormats = ['numeric', '2-digit'];
dayFormats.forEach((dayFormat: 'numeric' | '2-digit') => {
it(`date has correct day format: ${dayFormat}`, async () => {
const el = await fixture<SlFormatDate>(
html`
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" day="${dayFormat}"></sl-format-date>
`
);
const el = await fixture<SlFormatDate>(html`
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" day="${dayFormat}"></sl-format-date>
`);
const expected = new Intl.DateTimeFormat('en-US', { day: dayFormat }).format(
new Date(new Date().getFullYear(), 0, 1)
@ -159,11 +147,9 @@ describe('<sl-format-date>', () => {
const hourFormats = ['numeric', '2-digit'];
hourFormats.forEach((hourFormat: 'numeric' | '2-digit') => {
it(`date has correct hour format: ${hourFormat}`, async () => {
const el = await fixture<SlFormatDate>(
html`
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" hour="${hourFormat}"></sl-format-date>
`
);
const el = await fixture<SlFormatDate>(html`
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" hour="${hourFormat}"></sl-format-date>
`);
const expected = new Intl.DateTimeFormat('en-US', { hour: hourFormat }).format(
new Date(new Date().getFullYear(), 0, 1)
@ -177,14 +163,9 @@ describe('<sl-format-date>', () => {
const minuteFormats = ['numeric', '2-digit'];
minuteFormats.forEach((minuteFormat: 'numeric' | '2-digit') => {
it(`date has correct minute format: ${minuteFormat}`, async () => {
const el = await fixture<SlFormatDate>(
html`
<sl-format-date
.date="${new Date(new Date().getFullYear(), 0, 1)}"
minute="${minuteFormat}"
></sl-format-date>
`
);
const el = await fixture<SlFormatDate>(html`
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" minute="${minuteFormat}"></sl-format-date>
`);
const expected = new Intl.DateTimeFormat('en-US', { minute: minuteFormat }).format(
new Date(new Date().getFullYear(), 0, 1)
@ -198,14 +179,9 @@ describe('<sl-format-date>', () => {
const secondFormats = ['numeric', '2-digit'];
secondFormats.forEach((secondFormat: 'numeric' | '2-digit') => {
it(`date has correct second format: ${secondFormat}`, async () => {
const el = await fixture<SlFormatDate>(
html`
<sl-format-date
.date="${new Date(new Date().getFullYear(), 0, 1)}"
second="${secondFormat}"
></sl-format-date>
`
);
const el = await fixture<SlFormatDate>(html`
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" second="${secondFormat}"></sl-format-date>
`);
const expected = new Intl.DateTimeFormat('en-US', { second: secondFormat }).format(
new Date(new Date().getFullYear(), 0, 1)
@ -219,14 +195,12 @@ describe('<sl-format-date>', () => {
const timeZoneNameFormats = ['short', 'long'];
timeZoneNameFormats.forEach((timeZoneNameFormat: 'short' | 'long') => {
it(`date has correct timeZoneName format: ${timeZoneNameFormat}`, async () => {
const el = await fixture<SlFormatDate>(
html`
<sl-format-date
.date="${new Date(new Date().getFullYear(), 0, 1)}"
time-zone-name="${timeZoneNameFormat}"
></sl-format-date>
`
);
const el = await fixture<SlFormatDate>(html`
<sl-format-date
.date="${new Date(new Date().getFullYear(), 0, 1)}"
time-zone-name="${timeZoneNameFormat}"
></sl-format-date>
`);
const expected = new Intl.DateTimeFormat('en-US', { timeZoneName: timeZoneNameFormat }).format(
new Date(new Date().getFullYear(), 0, 1)
@ -240,14 +214,9 @@ describe('<sl-format-date>', () => {
const timeZones = ['America/New_York', 'America/Los_Angeles', 'Europe/Zurich'];
timeZones.forEach(timeZone => {
it(`date has correct timeZoneName format: ${timeZone}`, async () => {
const el = await fixture<SlFormatDate>(
html`
<sl-format-date
.date="${new Date(new Date().getFullYear(), 0, 1)}"
time-zone="${timeZone}"
></sl-format-date>
`
);
const el = await fixture<SlFormatDate>(html`
<sl-format-date .date="${new Date(new Date().getFullYear(), 0, 1)}" time-zone="${timeZone}"></sl-format-date>
`);
const expected = new Intl.DateTimeFormat('en-US', { timeZone: timeZone }).format(
new Date(new Date().getFullYear(), 0, 1)
@ -261,14 +230,12 @@ describe('<sl-format-date>', () => {
const hourFormatValues = ['auto', '12', '24'];
hourFormatValues.forEach(hourFormatValue => {
it(`date has correct hourFormat format: ${hourFormatValue}`, async () => {
const el = await fixture<SlFormatDate>(
html`
<sl-format-date
.date="${new Date(new Date().getFullYear(), 0, 1)}"
hour-format="${hourFormatValue as 'auto' | '12' | '24'}"
></sl-format-date>
`
);
const el = await fixture<SlFormatDate>(html`
<sl-format-date
.date="${new Date(new Date().getFullYear(), 0, 1)}"
hour-format="${hourFormatValue as 'auto' | '12' | '24'}"
></sl-format-date>
`);
const expected = new Intl.DateTimeFormat('en-US', {
hour12: hourFormatValue === 'auto' ? undefined : hourFormatValue === '12'

Wyświetl plik

@ -1,6 +1,6 @@
import '../../../dist/shoelace.js';
import { expect, fixture, html } from '@open-wc/testing';
import type SlFormatNumber from './format-number';
import type SlFormatNumber from './format-number.js';
describe('<sl-format-number>', () => {
describe('defaults ', () => {
@ -24,9 +24,9 @@ describe('<sl-format-number>', () => {
describe('lang property', () => {
['de', 'de-CH', 'fr', 'es', 'he', 'ja', 'nl', 'pl', 'pt', 'ru'].forEach(lang => {
it(`number has correct language format: ${lang}`, async () => {
const el = await fixture<SlFormatNumber>(
html` <sl-format-number value="1000" lang="${lang}"></sl-format-number> `
);
const el = await fixture<SlFormatNumber>(html`
<sl-format-number value="1000" lang="${lang}"></sl-format-number>
`);
const expected = new Intl.NumberFormat(lang, { style: 'decimal', useGrouping: true }).format(1000);
expect(el.shadowRoot?.textContent).to.equal(expected);
});
@ -36,9 +36,9 @@ describe('<sl-format-number>', () => {
describe('type property', () => {
['currency', 'decimal', 'percent'].forEach(type => {
it(`number has correct type format: ${type}`, async () => {
const el = await fixture<SlFormatNumber>(
html` <sl-format-number value="1000" type="${type}"></sl-format-number> `
);
const el = await fixture<SlFormatNumber>(html`
<sl-format-number value="1000" type="${type}"></sl-format-number>
`);
const expected = new Intl.NumberFormat('en-US', { style: type, currency: 'USD' }).format(1000);
expect(el.shadowRoot?.textContent).to.equal(expected);
});
@ -62,9 +62,9 @@ describe('<sl-format-number>', () => {
describe('currency property', () => {
['USD', 'CAD', 'AUD', 'UAH'].forEach(currency => {
it(`number has correct type format: ${currency}`, async () => {
const el = await fixture<SlFormatNumber>(
html` <sl-format-number value="1000" currency="${currency}"></sl-format-number> `
);
const el = await fixture<SlFormatNumber>(html`
<sl-format-number value="1000" currency="${currency}"></sl-format-number>
`);
const expected = new Intl.NumberFormat('en-US', { style: 'decimal', currency: currency }).format(1000);
expect(el.shadowRoot?.textContent).to.equal(expected);
});
@ -74,9 +74,9 @@ describe('<sl-format-number>', () => {
describe('currencyDisplay property', () => {
['symbol', 'narrowSymbol', 'code', 'name'].forEach(currencyDisplay => {
it(`number has correct type format: ${currencyDisplay}`, async () => {
const el = await fixture<SlFormatNumber>(
html` <sl-format-number value="1000" currency-display="${currencyDisplay}"></sl-format-number> `
);
const el = await fixture<SlFormatNumber>(html`
<sl-format-number value="1000" currency-display="${currencyDisplay}"></sl-format-number>
`);
const expected = new Intl.NumberFormat('en-US', { style: 'decimal', currencyDisplay: currencyDisplay }).format(
1000
);
@ -88,9 +88,9 @@ describe('<sl-format-number>', () => {
describe('minimumIntegerDigits property', () => {
[4, 5, 6].forEach(minDigits => {
it(`number has correct type format: ${minDigits}`, async () => {
const el = await fixture<SlFormatNumber>(
html` <sl-format-number value="1000" minimum-integer-digits="${minDigits}"></sl-format-number> `
);
const el = await fixture<SlFormatNumber>(html`
<sl-format-number value="1000" minimum-integer-digits="${minDigits}"></sl-format-number>
`);
const expected = new Intl.NumberFormat('en-US', {
style: 'decimal',
currencyDisplay: 'symbol',
@ -104,9 +104,9 @@ describe('<sl-format-number>', () => {
describe('minimumFractionDigits property', () => {
[4, 5, 6].forEach(minFractionDigits => {
it(`number has correct type format: ${minFractionDigits}`, async () => {
const el = await fixture<SlFormatNumber>(
html` <sl-format-number value="1000" minimum-fraction-digits="${minFractionDigits}"></sl-format-number> `
);
const el = await fixture<SlFormatNumber>(html`
<sl-format-number value="1000" minimum-fraction-digits="${minFractionDigits}"></sl-format-number>
`);
const expected = new Intl.NumberFormat('en-US', {
style: 'decimal',
currencyDisplay: 'symbol',
@ -120,9 +120,9 @@ describe('<sl-format-number>', () => {
describe('maximumFractionDigits property', () => {
[4, 5, 6].forEach(maxFractionDigits => {
it(`number has correct type format: ${maxFractionDigits}`, async () => {
const el = await fixture<SlFormatNumber>(
html` <sl-format-number value="1000" maximum-fraction-digits="${maxFractionDigits}"></sl-format-number> `
);
const el = await fixture<SlFormatNumber>(html`
<sl-format-number value="1000" maximum-fraction-digits="${maxFractionDigits}"></sl-format-number>
`);
const expected = new Intl.NumberFormat('en-US', {
style: 'decimal',
currencyDisplay: 'symbol',
@ -136,11 +136,9 @@ describe('<sl-format-number>', () => {
describe('minimumSignificantDigits property', () => {
[4, 5, 6].forEach(minSignificantDigits => {
it(`number has correct type format: ${minSignificantDigits}`, async () => {
const el = await fixture<SlFormatNumber>(
html`
<sl-format-number value="1000" minimum-significant-digits="${minSignificantDigits}"></sl-format-number>
`
);
const el = await fixture<SlFormatNumber>(html`
<sl-format-number value="1000" minimum-significant-digits="${minSignificantDigits}"></sl-format-number>
`);
const expected = new Intl.NumberFormat('en-US', {
style: 'decimal',
currencyDisplay: 'symbol',
@ -154,11 +152,9 @@ describe('<sl-format-number>', () => {
describe('maximumSignificantDigits property', () => {
[4, 5, 6].forEach(maxSignificantDigits => {
it(`number has correct type format: ${maxSignificantDigits}`, async () => {
const el = await fixture<SlFormatNumber>(
html`
<sl-format-number value="1000" maximum-significant-digits="${maxSignificantDigits}"></sl-format-number>
`
);
const el = await fixture<SlFormatNumber>(html`
<sl-format-number value="1000" maximum-significant-digits="${maxSignificantDigits}"></sl-format-number>
`);
const expected = new Intl.NumberFormat('en-US', {
style: 'decimal',
currencyDisplay: 'symbol',

Wyświetl plik

@ -2,6 +2,7 @@ import { classMap } from 'lit/directives/class-map.js';
import { html, literal } from 'lit/static-html.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { property, query, state } from 'lit/decorators.js';
import componentStyles from '../../styles/component.styles.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import SlIcon from '../icon/icon.component.js';
import styles from './icon-button.styles.js';
@ -21,7 +22,7 @@ import type { CSSResultGroup } from 'lit';
* @csspart base - The component's base wrapper.
*/
export default class SlIconButton extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static styles: CSSResultGroup = [componentStyles, styles];
static dependencies = { 'sl-icon': SlIcon };
@query('.icon-button') button: HTMLButtonElement | HTMLLinkElement;

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