Porównaj commity

...

155 Commity

Autor SHA1 Wiadomość Data
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
198 zmienionych plików z 8856 dodań i 7004 usunięć

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

2
.gitignore vendored
Wyświetl plik

@ -1,8 +1,6 @@
_site
.cache
.DS_Store
package.json
package-lock.json
cdn
dist
docs/assets/images/sprite.svg

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

@ -100,6 +100,7 @@
"monospace",
"mousedown",
"mousemove",
"mouseout",
"mouseup",
"multiselectable",
"nextjs",

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,15 +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

@ -1059,7 +1059,6 @@ html.sidebar-open #menu-toggle {
padding: 0.5rem;
margin: 0;
cursor: pointer;
transition: 250ms scale ease;
}
#theme-selector:not(:defined) {
@ -1102,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;
@ -1420,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

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

@ -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

@ -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

@ -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

@ -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

@ -35,35 +35,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 +94,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

@ -12,6 +12,86 @@ 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
- 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]

12488
package-lock.json wygenerowano

Plik diff jest za duży Load Diff

Wyświetl plik

@ -1,7 +1,7 @@
{
"name": "@shoelace-style/shoelace",
"description": "A forward-thinking library of web components.",
"version": "2.11.2",
"version": "2.15.0",
"homepage": "https://github.com/shoelace-style/shoelace",
"author": "Cory LaViska",
"license": "MIT",
@ -49,6 +49,7 @@
"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 --log-level=warn .",
"prettier:check": "prettier --check --log-level=warn .",
@ -84,9 +85,9 @@
"@typescript-eslint/eslint-plugin": "^6.7.5",
"@typescript-eslint/parser": "^6.7.5",
"@web/dev-server-esbuild": "^0.3.6",
"@web/test-runner": "^0.15.3",
"@web/test-runner-commands": "^0.6.6",
"@web/test-runner-playwright": "^0.9.0",
"@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.3.0",
@ -94,8 +95,9 @@
"command-line-args": "^5.2.1",
"comment-parser": "^1.4.0",
"cspell": "^6.18.1",
"custom-element-jet-brains-integration": "^1.2.1",
"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.19.4",

Wyświetl plik

@ -25,10 +25,10 @@ for await (const component of components) {
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/react';` : ``;
const events = (component.events || [])

Wyświetl plik

@ -47,9 +47,19 @@ files.forEach(async 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

@ -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;

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;
}

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;
}

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;
@ -549,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)
)
@ -587,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

@ -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

@ -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,7 +16,7 @@ import type { CSSResultGroup } from 'lit';
*
*/
export default class SlCarouselItem extends ShoelaceElement {
static styles: CSSResultGroup = styles;
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;

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

@ -3,14 +3,15 @@ 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 SlIcon from '../icon/icon.component.js';
import styles from './carousel.styles.js';
@ -47,7 +48,7 @@ import type SlCarouselItem from '../carousel-item/carousel-item.component.js';
* 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. */
@ -86,11 +87,11 @@ export default class SlCarousel extends ShoelaceElement {
// 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 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,35 +99,10 @@ 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();
}
@ -216,25 +192,127 @@ export default class SlCarousel extends ShoelaceElement {
}
}
private handleScrollEnd() {
const slides = this.getSlides();
const entries = [...this.intersectionObserverEntries.values()];
private handleMouseDragStart(event: PointerEvent) {
const canDrag = this.mouseDragging && event.button === 0;
if (canDrag) {
event.preventDefault();
const firstIntersecting: IntersectionObserverEntry | undefined = entries.find(entry => entry.isIntersecting);
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, 'auto');
} else if (firstIntersecting) {
// 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;
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() {
if (!this.scrolling || this.dragging) return;
this.synchronizeSlides();
this.scrolling = false;
}
private isCarouselItem(node: Node): node is SlCarouselItem {
return node instanceof Element && node.tagName.toLowerCase() === 'sl-carousel-item';
}
@ -257,14 +335,8 @@ export default class SlCarousel extends ShoelaceElement {
@watch('loop', { waitUntilFirstUpdate: true })
@watch('slidesPerPage', { waitUntilFirstUpdate: true })
initializeSlides() {
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));
@ -281,9 +353,7 @@ export default class SlCarousel extends ShoelaceElement {
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');
@ -350,11 +420,6 @@ export default class SlCarousel extends ShoelaceElement {
}
}
@watch('mouseDragging')
handleMouseDraggingChange() {
this.scrollController.mouseDragging = this.mouseDragging;
}
/**
* Move the carousel backward by `slides-per-move` slides.
*
@ -380,7 +445,7 @@ 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 });
@ -399,18 +464,26 @@ 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, slidesPerMove } = this;
const { slidesPerMove, scrolling } = this;
const pagesCount = this.getPageCount();
const currentPage = this.getCurrentPage();
const prevEnabled = this.canScrollPrev();
@ -425,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>

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,12 +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`
@ -29,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`
@ -47,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;
@ -68,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;
@ -91,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;
@ -178,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
@ -304,6 +329,7 @@ describe('<sl-carousel>', () => {
await clickOnElement(nextButton);
await oneEvent(el.scrollContainer, 'scrollend');
await intersectionObserverCallbacks();
await el.updateComplete;
// Assert
@ -330,13 +356,19 @@ describe('<sl-carousel>', () => {
// 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
@ -409,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 () => {
@ -421,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;
@ -444,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
@ -483,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);
@ -508,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;
@ -532,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
@ -566,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);
@ -580,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-page="2" 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);
});
});
@ -600,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-page="2" 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);
});
});
@ -631,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,140 +0,0 @@
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;
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('scrollend', this.handleScrollEnd, true);
scrollContainer.addEventListener('pointerdown', this.handlePointerDown);
scrollContainer.addEventListener('pointerup', this.handlePointerUp);
scrollContainer.addEventListener('pointercancel', this.handlePointerUp);
}
hostDisconnected(): void {
const host = this.host;
const scrollContainer = host.scrollContainer;
scrollContainer.removeEventListener('scroll', this.handleScroll);
scrollContainer.removeEventListener('scrollend', this.handleScrollEnd, true);
scrollContainer.removeEventListener('pointerdown', this.handlePointerDown);
scrollContainer.removeEventListener('pointerup', this.handlePointerUp);
scrollContainer.removeEventListener('pointercancel', this.handlePointerUp);
}
handleScroll = () => {
if (!this.scrolling) {
this.scrolling = true;
this.host.requestUpdate();
}
};
handleScrollEnd = () => {
if (this.scrolling && !this.dragging) {
this.scrolling = false;
this.host.requestUpdate();
}
};
handlePointerDown = (event: PointerEvent) => {
// Do not handle drag for touch interactions as scroll is natively supported
if (event.pointerType === 'touch') {
return;
}
const canDrag = this.mouseDragging && 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.host.scrollContainer.releasePointerCapture(event.pointerId);
this.handleDragEnd();
};
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
});
}
handleDragEnd() {
const host = this.host;
const scrollContainer = host.scrollContainer;
scrollContainer.removeEventListener('pointermove', this.handlePointerMove);
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' });
// Wait for scroll to be applied
requestAnimationFrame(async () => {
if (startLeft !== finalLeft || startTop !== finalTop) {
await waitForEvent(scrollContainer, 'scrollend');
}
scrollContainer.style.removeProperty('scroll-snap-type');
this.dragging = false;
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;
}
@ -118,6 +115,7 @@ export default css`
: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;

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 () => {
@ -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

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

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

@ -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';
@ -66,7 +67,7 @@ import type { CSSResultGroup } from 'lit';
* 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
};
@ -75,6 +76,7 @@ export default class SlDialog extends ShoelaceElement {
private readonly localize = new LocalizeController(this);
private originalTrigger: HTMLElement | null;
public modal = new Modal(this);
private closeWatcher: CloseWatcher | null;
@query('.dialog') dialog: HTMLElement;
@query('.dialog__panel') panel: HTMLElement;
@ -112,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') {
@ -130,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);
}
@ -300,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 () => {
@ -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';
@ -74,13 +75,14 @@ import type { CSSResultGroup } from 'lit';
* 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 originalTrigger: HTMLElement | null;
public modal = new Modal(this);
private closeWatcher: CloseWatcher | null;
@query('.drawer') drawer: HTMLElement;
@query('.drawer__panel') panel: HTMLElement;
@ -129,6 +131,7 @@ export default class SlDrawer extends ShoelaceElement {
disconnectedCallback() {
super.disconnectedCallback();
unlockBodyScrolling(this);
this.closeWatcher?.destroy();
}
private requestClose(source: 'close-button' | 'keyboard' | 'overlay') {
@ -147,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 () => {

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 ', () => {

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 ', () => {

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;

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;
color: var(--sl-color-neutral-600);

Wyświetl plik

@ -1,7 +1,7 @@
import '../../../dist/shoelace.js';
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import sinon from 'sinon';
import type SlIconButton from './icon-button';
import type SlIconButton from './icon-button.js';
type LinkTarget = '_self' | '_blank' | '_parent' | '_top';

Wyświetl plik

@ -3,9 +3,9 @@ import { html } from 'lit';
import { isTemplateResult } from 'lit/directive-helpers.js';
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 styles from './icon.styles.js';
import type { CSSResultGroup, HTMLTemplateResult } from 'lit';
const CACHEABLE_ERROR = Symbol();
@ -33,7 +33,7 @@ interface IconSource {
* @csspart use - The <use> element generated when using `spriteSheet: true`
*/
export default class SlIcon extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static styles: CSSResultGroup = [componentStyles, styles];
private initialRender = false;
@ -42,9 +42,21 @@ export default class SlIcon extends ShoelaceElement {
let fileData: Response;
if (library?.spriteSheet) {
return html`<svg part="svg">
this.svg = html`<svg part="svg">
<use part="use" href="${url}"></use>
</svg>`;
// Using a templateResult requires the SVG to be written to the DOM first before we can grab the SVGElement
// to be passed to the library's mutator function.
await this.updateComplete;
const svg = this.shadowRoot!.querySelector("[part='svg']")!;
if (typeof library.mutator === 'function') {
library.mutator(svg as SVGElement);
}
return this.svg;
}
try {

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;
width: 1em;

Wyświetl plik

@ -1,8 +1,8 @@
import { aTimeout, elementUpdated, expect, fixture, html, oneEvent } from '@open-wc/testing';
import { registerIconLibrary } from '../../../dist/shoelace.js';
import type { SlErrorEvent } from '../../events/sl-error';
import type { SlLoadEvent } from '../../events/sl-load';
import type SlIcon from './icon';
import type { SlErrorEvent } from '../../events/sl-error.js';
import type { SlLoadEvent } from '../../events/sl-load.js';
import type SlIcon from './icon.js';
const testLibraryIcons = {
'test-icon1': `
@ -204,6 +204,10 @@ describe('<sl-icon>', () => {
const rect = use?.getBoundingClientRect();
expect(rect?.width).to.equal(0);
expect(rect?.width).to.equal(0);
// Make sure the mutator is applied.
// https://github.com/shoelace-style/shoelace/issues/1925
expect(svg?.getAttribute('fill')).to.equal('currentColor');
});
// TODO: <use> svg icons don't emit a "load" or "error" event...if we can figure out how to get the event to emit errors.

Wyświetl plik

@ -41,8 +41,8 @@ const icons = {
</svg>
`,
copy: `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-files" viewBox="0 0 16 16" part="svg">
<path d="M13 0H6a2 2 0 0 0-2 2 2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2 2 2 0 0 0 2-2V2a2 2 0 0 0-2-2zm0 13V4a2 2 0 0 0-2-2H5a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1zM3 4a1 1 0 0 1 1-1h7a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4z"></path>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-copy" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M4 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V2Zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H6ZM2 5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-1h1v1a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h1v1H2Z"/>
</svg>
`,
eye: `

Wyświetl plik

@ -6,6 +6,7 @@ import { LocalizeController } from '../../utilities/localize.js';
import { property, query } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.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 './image-comparer.styles.js';
@ -35,7 +36,7 @@ import type { CSSResultGroup } from 'lit';
* @cssproperty --handle-size - The size of the compare handle.
*/
export default class SlImageComparer extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static styles: CSSResultGroup = [componentStyles, styles];
static scopedElement = { '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}
:host {
--divider-width: 2px;
--handle-size: 2.5rem;

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 SlImageComparer from './image-comparer';
import type SlImageComparer from './image-comparer.js';
describe('<sl-image-comparer>', () => {
it('should render a basic before/after', async () => {
@ -231,8 +231,8 @@ describe('<sl-image-comparer>', () => {
const handle = el.shadowRoot!.querySelector<HTMLElement>('[part~="handle"]')!;
const base = el.shadowRoot!.querySelector<HTMLElement>('[part~="base"]')!;
const rect = base.getBoundingClientRect();
const offsetX = rect.left + window.pageXOffset;
const offsetY = rect.top + window.pageYOffset;
const offsetX = rect.left + window.scrollX;
const offsetY = rect.top + window.scrollY;
handle.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));

Wyświetl plik

@ -2,6 +2,7 @@ import { html } from 'lit';
import { property } from 'lit/decorators.js';
import { requestInclude } from './request.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import styles from './include.styles.js';
import type { CSSResultGroup } from 'lit';
@ -16,7 +17,7 @@ import type { CSSResultGroup } from 'lit';
* @event {{ status: number }} sl-error - Emitted when the included file fails to load due to an error.
*/
export default class SlInclude extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static styles: CSSResultGroup = [componentStyles, styles];
/**
* The location of the HTML file to include. Be sure you trust the content you are including as it will be executed as

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

@ -1,7 +1,7 @@
import '../../../dist/shoelace.js';
import { aTimeout, expect, fixture, html, waitUntil } from '@open-wc/testing';
import sinon from 'sinon';
import type SlInclude from './include';
import type SlInclude from './include.js';
const stubbedFetchResponse: Response = {
headers: new Headers(),

Wyświetl plik

@ -8,6 +8,8 @@ import { live } from 'lit/directives/live.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 formControlStyles from '../../styles/form-control.styles.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import SlIcon from '../icon/icon.component.js';
import styles from './input.styles.js';
@ -49,7 +51,7 @@ import type { ShoelaceFormControl } from '../../internal/shoelace-element.js';
* @csspart suffix - The container that wraps the suffix.
*/
export default class SlInput 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, {
@ -249,13 +251,16 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
}
private handleClearClick(event: MouseEvent) {
this.value = '';
this.emit('sl-clear');
this.emit('sl-input');
this.emit('sl-change');
this.input.focus();
event.preventDefault();
event.stopPropagation();
if (this.value !== '') {
this.value = '';
this.emit('sl-clear');
this.emit('sl-input');
this.emit('sl-change');
}
this.input.focus();
}
private handleFocus() {
@ -347,10 +352,12 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
replacement: string,
start?: number,
end?: number,
selectMode?: 'select' | 'start' | 'end' | 'preserve'
selectMode: 'select' | 'start' | 'end' | 'preserve' = 'preserve'
) {
// @ts-expect-error - start, end, and selectMode are optional
this.input.setRangeText(replacement, start, end, selectMode);
const selectionStart = start ?? this.input.selectionStart!;
const selectionEnd = end ?? this.input.selectionEnd!;
this.input.setRangeText(replacement, selectionStart, selectionEnd, selectMode);
if (this.value !== this.input.value) {
this.value = this.input.value;
@ -489,14 +496,11 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
@blur=${this.handleBlur}
/>
${hasClearIcon
${isClearIconVisible
? html`
<button
part="clear-button"
class=${classMap({
input__clear: true,
'input__clear--visible': isClearIconVisible
})}
class="input__clear"
type="button"
aria-label=${this.localize.term('clearEntry')}
@click=${this.handleClearClick}

Wyświetl plik

@ -1,11 +1,6 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles.js';
import formControlStyles from '../../styles/form-control.styles.js';
export default css`
${componentStyles}
${formControlStyles}
:host {
display: block;
}
@ -252,10 +247,6 @@ export default css`
* Clearable + Password Toggle
*/
.input__clear:not(.input__clear--visible) {
visibility: hidden;
}
.input__clear,
.input__password-toggle {
display: inline-flex;
@ -280,10 +271,6 @@ export default css`
outline: none;
}
.input--empty .input__clear {
visibility: hidden;
}
/* Don't show the browser's password toggle in Edge */
::-ms-reveal {
display: none;

Wyświetl plik

@ -4,7 +4,7 @@ import { getFormControls, serialize } from '../../../dist/shoelace.js';
import { runFormControlBaseTests } from '../../internal/test/form-control-base-tests.js';
import { sendKeys } from '@web/test-runner-commands'; // must come from the same module
import sinon from 'sinon';
import type SlInput from './input';
import type SlInput from './input.js';
describe('<sl-input>', () => {
it('should pass accessibility tests', async () => {
@ -545,5 +545,17 @@ describe('<sl-input>', () => {
});
});
describe('when using the setRangeText() function', () => {
it('should set replacement text in the correct location', async () => {
const el = await fixture<SlInput>(html` <sl-input value="test"></sl-input> `);
el.focus();
el.setSelectionRange(1, 3);
el.setRangeText('boom');
await el.updateComplete;
expect(el.value).to.equal('tboomt'); // cspell:disable-line
});
});
runFormControlBaseTests('sl-input');
});

Wyświetl plik

@ -5,9 +5,11 @@ import { LocalizeController } from '../../utilities/localize.js';
import { property, query } from 'lit/decorators.js';
import { SubmenuController } from './submenu-controller.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 SlPopup from '../popup/popup.component.js';
import SlSpinner from '../spinner/spinner.component.js';
import styles from './menu-item.styles.js';
import type { CSSResultGroup } from 'lit';
@ -19,6 +21,7 @@ import type { CSSResultGroup } from 'lit';
*
* @dependency sl-icon
* @dependency sl-popup
* @dependency sl-spinner
*
* @slot - The menu item's label.
* @slot prefix - Used to prepend an icon or similar element to the menu item.
@ -30,15 +33,18 @@ import type { CSSResultGroup } from 'lit';
* @csspart prefix - The prefix container.
* @csspart label - The menu item label.
* @csspart suffix - The suffix container.
* @csspart spinner - The spinner that shows when the menu item is in the loading state.
* @csspart spinner__base - The spinner's base part.
* @csspart submenu-icon - The submenu icon, visible only when the menu item has a submenu (not yet implemented).
*
* @cssproperty [--submenu-offset=-2px] - The distance submenus shift to overlap the parent menu.
*/
export default class SlMenuItem extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static styles: CSSResultGroup = [componentStyles, styles];
static dependencies = {
'sl-icon': SlIcon,
'sl-popup': SlPopup
'sl-popup': SlPopup,
'sl-spinner': SlSpinner
};
private cachedTextLabel: string;
@ -55,6 +61,9 @@ export default class SlMenuItem extends ShoelaceElement {
/** A unique value to store in the menu item. This can be used as a way to identify menu items when selected. */
@property() value = '';
/** Draws the menu item in a loading state. */
@property({ type: Boolean, reflect: true }) loading = false;
/** Draws the menu item in a disabled state, preventing selection. */
@property({ type: Boolean, reflect: true }) disabled = false;
@ -158,6 +167,7 @@ export default class SlMenuItem extends ShoelaceElement {
'menu-item--rtl': isRtl,
'menu-item--checked': this.checked,
'menu-item--disabled': this.disabled,
'menu-item--loading': this.loading,
'menu-item--has-submenu': this.isSubmenu(),
'menu-item--submenu-expanded': isSubmenuExpanded
})}
@ -179,6 +189,7 @@ export default class SlMenuItem extends ShoelaceElement {
</span>
${this.submenuController.renderSubmenu()}
${this.loading ? html` <sl-spinner part="spinner" exportparts="base:spinner__base"></sl-spinner> ` : ''}
</div>
`;
}

Wyświetl plik

@ -1,20 +1,9 @@
import { css } from 'lit';
import componentStyles from '../../styles/component.styles.js';
export default css`
${componentStyles}
:host {
--submenu-offset: -2px;
/* Private */
--safe-triangle-cursor-x: 0;
--safe-triangle-cursor-y: 0;
--safe-triangle-submenu-start-x: 0;
--safe-triangle-submenu-start-y: 0;
--safe-triangle-submenu-end-x: 0;
--safe-triangle-submenu-end-y: 0;
display: block;
}
@ -46,6 +35,25 @@ export default css`
cursor: not-allowed;
}
.menu-item.menu-item--loading {
outline: none;
cursor: wait;
}
.menu-item.menu-item--loading *:not(sl-spinner) {
opacity: 0.5;
}
.menu-item--loading sl-spinner {
--indicator-color: currentColor;
--track-width: 1px;
position: absolute;
font-size: 0.75em;
top: calc(50% - 0.5em);
left: 0.65rem;
opacity: 1;
}
.menu-item .menu-item__label {
flex: 1 1 auto;
display: inline-block;
@ -83,9 +91,9 @@ export default css`
bottom: 0;
left: 0;
clip-path: polygon(
var(--safe-triangle-cursor-x) var(--safe-triangle-cursor-y),
var(--safe-triangle-submenu-start-x) var(--safe-triangle-submenu-start-y),
var(--safe-triangle-submenu-end-x) var(--safe-triangle-submenu-end-y)
var(--safe-triangle-cursor-x, 0) var(--safe-triangle-cursor-y, 0),
var(--safe-triangle-submenu-start-x, 0) var(--safe-triangle-submenu-start-y, 0),
var(--safe-triangle-submenu-end-x, 0) var(--safe-triangle-submenu-end-y, 0)
);
}
@ -139,4 +147,9 @@ export default css`
outline-offset: -1px;
}
}
::slotted(sl-menu) {
max-width: var(--auto-size-available-width) !important;
max-height: var(--auto-size-available-height) !important;
}
`;

Wyświetl plik

@ -2,8 +2,8 @@ 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 { SlSelectEvent } from '../../events/sl-select';
import type SlMenuItem from './menu-item';
import type { SlSelectEvent } from '../../events/sl-select.js';
import type SlMenuItem from './menu-item.js';
describe('<sl-menu-item>', () => {
it('should pass accessibility tests', async () => {
@ -40,6 +40,7 @@ describe('<sl-menu-item>', () => {
expect(el.value).to.equal('');
expect(el.disabled).to.be.false;
expect(el.loading).to.equal(false);
expect(el.getAttribute('aria-disabled')).to.equal('false');
});
@ -48,6 +49,13 @@ describe('<sl-menu-item>', () => {
expect(el.getAttribute('aria-disabled')).to.equal('true');
});
describe('when loading', () => {
it('should have a spinner present', async () => {
const el = await fixture<SlMenuItem>(html` <sl-menu-item loading>Menu Item Label</sl-menu-item> `);
expect(el.shadowRoot!.querySelector('sl-spinner')).to.exist;
});
});
it('should return a text label when calling getTextLabel()', async () => {
const el = await fixture<SlMenuItem>(html` <sl-menu-item>Test</sl-menu-item> `);
expect(el.getTextLabel()).to.equal('Test');

Wyświetl plik

@ -229,6 +229,7 @@ export class SubmenuController implements ReactiveController {
// newly opened menu.
private enableSubmenu(delay = true) {
if (delay) {
window.clearTimeout(this.enableSubmenuTimer);
this.enableSubmenuTimer = window.setTimeout(() => {
this.setSubmenuState(true);
}, this.submenuOpenDelay);
@ -238,7 +239,7 @@ export class SubmenuController implements ReactiveController {
}
private disableSubmenu() {
clearTimeout(this.enableSubmenuTimer);
window.clearTimeout(this.enableSubmenuTimer);
this.setSubmenuState(false);
}
@ -282,6 +283,8 @@ export class SubmenuController implements ReactiveController {
flip-fallback-strategy="best-fit"
skidding="${this.skidding}"
strategy="fixed"
auto-size="vertical"
auto-size-padding="10"
>
<slot name="submenu"></slot>
</sl-popup>

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 './menu-label.styles.js';
import type { CSSResultGroup } from 'lit';
@ -14,7 +15,7 @@ import type { CSSResultGroup } from 'lit';
* @csspart base - The component's base wrapper.
*/
export default class SlMenuLabel extends ShoelaceElement {
static styles: CSSResultGroup = styles;
static styles: CSSResultGroup = [componentStyles, styles];
render() {
return html` <slot part="base" class="menu-label"></slot> `;

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