diff --git a/docs/assets/styles/docs.css b/docs/assets/styles/docs.css index 989a2f39..3afdc528 100644 --- a/docs/assets/styles/docs.css +++ b/docs/assets/styles/docs.css @@ -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; diff --git a/docs/pages/components/checkbox.md b/docs/pages/components/checkbox.md index dd88b3f6..f5197b9b 100644 --- a/docs/pages/components/checkbox.md +++ b/docs/pages/components/checkbox.md @@ -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 +Label +``` + +```jsx:react +import SlCheckbox from '@shoelace-style/shoelace/dist/react/checkbox'; + +const App = () => Label; +``` + ### 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. diff --git a/docs/pages/components/switch.md b/docs/pages/components/switch.md index 31630c06..194a51cf 100644 --- a/docs/pages/components/switch.md +++ b/docs/pages/components/switch.md @@ -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 +Label +``` + +```jsx:react +import SlSwitch from '@shoelace-style/shoelace/dist/react/checkbox'; + +const App = () => Label; +``` + ### Custom Styles Use the available custom properties to change how the switch is styled. diff --git a/docs/pages/resources/changelog.md b/docs/pages/resources/changelog.md index 29cf2233..42aec6e5 100644 --- a/docs/pages/resources/changelog.md +++ b/docs/pages/resources/changelog.md @@ -12,6 +12,15 @@ Components with the Experimental 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). +## 2.14.0 + +- Added the Arabic translation [#1852] +- Added help text to `` [#1860] +- Added help text to `` [#1800] +- Fixed a bug in `` that caused HTML tags to be included in `getTextLabel()` +- Fixed a bug in `` 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 `` elements [#1835] diff --git a/package-lock.json b/package-lock.json index 3b4f2171..11bf225a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,12 @@ { "name": "@shoelace-style/shoelace", - "version": "2.13.1", + "version": "2.14.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@shoelace-style/shoelace", - "version": "2.13.1", - "hasInstallScript": true, + "version": "2.14.0", "license": "MIT", "dependencies": { "@ctrl/tinycolor": "^4.0.2", diff --git a/package.json b/package.json index ea058a32..01e3f443 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@shoelace-style/shoelace", "description": "A forward-thinking library of web components.", - "version": "2.13.1", + "version": "2.14.0", "homepage": "https://github.com/shoelace-style/shoelace", "author": "Cory LaViska", "license": "MIT", @@ -49,7 +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", - "postinstall": "npx playwright install", + "prepare": "npx playwright install", "prepublishOnly": "npm run verify", "prettier": "prettier --write --log-level=warn .", "prettier:check": "prettier --check --log-level=warn .", diff --git a/scripts/plop/templates/component/component.hbs b/scripts/plop/templates/component/component.hbs index a5e752c7..dc49d609 100644 --- a/scripts/plop/templates/component/component.hbs +++ b/scripts/plop/templates/component/component.hbs @@ -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); diff --git a/scripts/plop/templates/component/styles.hbs b/scripts/plop/templates/component/styles.hbs index 1775f7f7..940a1557 100644 --- a/scripts/plop/templates/component/styles.hbs +++ b/scripts/plop/templates/component/styles.hbs @@ -1,9 +1,6 @@ import { css } from 'lit'; -import componentStyles from '../../styles/component.styles.js'; export default css` - ${componentStyles} - :host { display: block; } diff --git a/src/components/alert/alert.component.ts b/src/components/alert/alert.component.ts index 5fd0722c..13d8e52f 100644 --- a/src/components/alert/alert.component.ts +++ b/src/components/alert/alert.component.ts @@ -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; diff --git a/src/components/alert/alert.styles.ts b/src/components/alert/alert.styles.ts index eb955cde..884230c5 100644 --- a/src/components/alert/alert.styles.ts +++ b/src/components/alert/alert.styles.ts @@ -1,9 +1,6 @@ import { css } from 'lit'; -import componentStyles from '../../styles/component.styles.js'; export default css` - ${componentStyles} - :host { display: contents; diff --git a/src/components/animated-image/animated-image.component.ts b/src/components/animated-image/animated-image.component.ts index d301618d..c5a57474 100644 --- a/src/components/animated-image/animated-image.component.ts +++ b/src/components/animated-image/animated-image.component.ts @@ -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 ``. * @slot pause-icon - Optional pause icon to use instead of the default. Works best with ``. * - * @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; diff --git a/src/components/animated-image/animated-image.styles.ts b/src/components/animated-image/animated-image.styles.ts index 3f326284..a32eda72 100644 --- a/src/components/animated-image/animated-image.styles.ts +++ b/src/components/animated-image/animated-image.styles.ts @@ -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); diff --git a/src/components/animation/animation.component.ts b/src/components/animation/animation.component.ts index 96a954e1..3a0d3257 100644 --- a/src/components/animation/animation.component.ts +++ b/src/components/animation/animation.component.ts @@ -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 `` elements. */ export default class SlAnimation extends ShoelaceElement { - static styles: CSSResultGroup = styles; + static styles: CSSResultGroup = [componentStyles, styles]; private animation?: Animation; private hasStarted = false; diff --git a/src/components/animation/animation.styles.ts b/src/components/animation/animation.styles.ts index 674851ee..1ef4bf6f 100644 --- a/src/components/animation/animation.styles.ts +++ b/src/components/animation/animation.styles.ts @@ -1,9 +1,6 @@ import { css } from 'lit'; -import componentStyles from '../../styles/component.styles.js'; export default css` - ${componentStyles} - :host { display: contents; } diff --git a/src/components/avatar/avatar.component.ts b/src/components/avatar/avatar.component.ts index 6a37ae5b..7b9285ec 100644 --- a/src/components/avatar/avatar.component.ts +++ b/src/components/avatar/avatar.component.ts @@ -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 }; diff --git a/src/components/avatar/avatar.styles.ts b/src/components/avatar/avatar.styles.ts index 45c27df5..a04983ef 100644 --- a/src/components/avatar/avatar.styles.ts +++ b/src/components/avatar/avatar.styles.ts @@ -1,9 +1,6 @@ import { css } from 'lit'; -import componentStyles from '../../styles/component.styles.js'; export default css` - ${componentStyles} - :host { display: inline-block; diff --git a/src/components/badge/badge.component.ts b/src/components/badge/badge.component.ts index 9232bf61..7d8f262e 100644 --- a/src/components/badge/badge.component.ts +++ b/src/components/badge/badge.component.ts @@ -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'; diff --git a/src/components/badge/badge.styles.ts b/src/components/badge/badge.styles.ts index 740e6bd7..8da6ad1a 100644 --- a/src/components/badge/badge.styles.ts +++ b/src/components/badge/badge.styles.ts @@ -1,9 +1,6 @@ import { css } from 'lit'; -import componentStyles from '../../styles/component.styles.js'; export default css` - ${componentStyles} - :host { display: inline-flex; } diff --git a/src/components/breadcrumb-item/breadcrumb-item.component.ts b/src/components/breadcrumb-item/breadcrumb-item.component.ts index 53e1a8fe..5c81c15b 100644 --- a/src/components/breadcrumb-item/breadcrumb-item.component.ts +++ b/src/components/breadcrumb-item/breadcrumb-item.component.ts @@ -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'); diff --git a/src/components/breadcrumb-item/breadcrumb-item.styles.ts b/src/components/breadcrumb-item/breadcrumb-item.styles.ts index fcf1f211..f6c5ca60 100644 --- a/src/components/breadcrumb-item/breadcrumb-item.styles.ts +++ b/src/components/breadcrumb-item/breadcrumb-item.styles.ts @@ -1,9 +1,6 @@ import { css } from 'lit'; -import componentStyles from '../../styles/component.styles.js'; export default css` - ${componentStyles} - :host { display: inline-flex; } diff --git a/src/components/breadcrumb/breadcrumb.component.ts b/src/components/breadcrumb/breadcrumb.component.ts index fb481aca..4f314935 100644 --- a/src/components/breadcrumb/breadcrumb.component.ts +++ b/src/components/breadcrumb/breadcrumb.component.ts @@ -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); diff --git a/src/components/breadcrumb/breadcrumb.styles.ts b/src/components/breadcrumb/breadcrumb.styles.ts index 8974f84a..86c0e16e 100644 --- a/src/components/breadcrumb/breadcrumb.styles.ts +++ b/src/components/breadcrumb/breadcrumb.styles.ts @@ -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; diff --git a/src/components/button-group/button-group.component.ts b/src/components/button-group/button-group.component.ts index 84328a78..0c7d1ca6 100644 --- a/src/components/button-group/button-group.component.ts +++ b/src/components/button-group/button-group.component.ts @@ -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; diff --git a/src/components/button-group/button-group.styles.ts b/src/components/button-group/button-group.styles.ts index 868f5a83..89e6de8d 100644 --- a/src/components/button-group/button-group.styles.ts +++ b/src/components/button-group/button-group.styles.ts @@ -1,9 +1,6 @@ import { css } from 'lit'; -import componentStyles from '../../styles/component.styles.js'; export default css` - ${componentStyles} - :host { display: inline-block; } diff --git a/src/components/button/button.component.ts b/src/components/button/button.component.ts index 262dcd72..54ecbb03 100644 --- a/src/components/button/button.component.ts +++ b/src/components/button/button.component.ts @@ -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,7 +39,7 @@ 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 diff --git a/src/components/button/button.styles.ts b/src/components/button/button.styles.ts index 73016b3d..c69ef200 100644 --- a/src/components/button/button.styles.ts +++ b/src/components/button/button.styles.ts @@ -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; diff --git a/src/components/card/card.component.ts b/src/components/card/card.component.ts index e9071906..3890cd98 100644 --- a/src/components/card/card.component.ts +++ b/src/components/card/card.component.ts @@ -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'); diff --git a/src/components/card/card.styles.ts b/src/components/card/card.styles.ts index b334c126..9671516c 100644 --- a/src/components/card/card.styles.ts +++ b/src/components/card/card.styles.ts @@ -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); diff --git a/src/components/carousel-item/carousel-item.component.ts b/src/components/carousel-item/carousel-item.component.ts index 274c5f59..7ef2ba16 100644 --- a/src/components/carousel-item/carousel-item.component.ts +++ b/src/components/carousel-item/carousel-item.component.ts @@ -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(); diff --git a/src/components/carousel-item/carousel-item.styles.ts b/src/components/carousel-item/carousel-item.styles.ts index 4a505374..11e07af3 100644 --- a/src/components/carousel-item/carousel-item.styles.ts +++ b/src/components/carousel-item/carousel-item.styles.ts @@ -1,9 +1,6 @@ import { css } from 'lit'; -import componentStyles from '../../styles/component.styles.js'; export default css` - ${componentStyles} - :host { --aspect-ratio: inherit; diff --git a/src/components/carousel/carousel.component.ts b/src/components/carousel/carousel.component.ts index 5e186fc9..21f47bd7 100644 --- a/src/components/carousel/carousel.component.ts +++ b/src/components/carousel/carousel.component.ts @@ -11,6 +11,7 @@ import { prefersReducedMotion } from '../../internal/animate.js'; import { range } from 'lit/directives/range.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. */ @@ -476,7 +477,7 @@ export default class SlCarousel extends ShoelaceElement { this.scrollToSlide(nextSlide, prefersReducedMotion() ? 'auto' : behavior); } - private async scrollToSlide(slide: HTMLElement, behavior: ScrollBehavior = 'smooth') { + private scrollToSlide(slide: HTMLElement, behavior: ScrollBehavior = 'smooth') { const scrollContainer = this.scrollContainer; const scrollContainerRect = scrollContainer.getBoundingClientRect(); const nextSlideRect = slide.getBoundingClientRect(); @@ -484,16 +485,11 @@ export default class SlCarousel extends ShoelaceElement { const nextLeft = nextSlideRect.left - scrollContainerRect.left; const nextTop = nextSlideRect.top - scrollContainerRect.top; - // If the slide is already in view, don't need to scroll - if (nextLeft !== scrollContainer.scrollLeft || nextTop !== scrollContainer.scrollTop) { - scrollContainer.scrollTo({ - left: nextLeft + scrollContainer.scrollLeft, - top: nextTop + scrollContainer.scrollTop, - behavior - }); - - await waitForEvent(scrollContainer, 'scrollend'); - } + scrollContainer.scrollTo({ + left: nextLeft + scrollContainer.scrollLeft, + top: nextTop + scrollContainer.scrollTop, + behavior + }); } render() { diff --git a/src/components/carousel/carousel.styles.ts b/src/components/carousel/carousel.styles.ts index a26c9446..33ee20eb 100644 --- a/src/components/carousel/carousel.styles.ts +++ b/src/components/carousel/carousel.styles.ts @@ -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; diff --git a/src/components/carousel/carousel.test.ts b/src/components/carousel/carousel.test.ts index 42d693a7..01cfc8d1 100644 --- a/src/components/carousel/carousel.test.ts +++ b/src/components/carousel/carousel.test.ts @@ -1,6 +1,6 @@ import '../../../dist/shoelace.js'; import { clickOnElement, dragElement, moveMouseOnElement } from '../../internal/test.js'; -import { expect, fixture, html, oneEvent } from '@open-wc/testing'; +import { expect, fixture, html, nextFrame, oneEvent } from '@open-wc/testing'; import { map } from 'lit/directives/map.js'; import { range } from 'lit/directives/range.js'; import { resetMouse } from '@web/test-runner-commands'; @@ -8,10 +8,16 @@ import sinon from 'sinon'; import type SlCarousel from './carousel.js'; describe('', () => { + const sandbox = sinon.createSandbox(); + afterEach(async () => { await resetMouse(); }); + afterEach(() => { + sandbox.restore(); + }); + it('should render a carousel with default configuration', async () => { // Arrange const el = await fixture(html` @@ -34,15 +40,11 @@ describe('', () => { 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(html` @@ -52,7 +54,7 @@ describe('', () => { Node 3 `); - sinon.stub(el, 'next'); + sandbox.stub(el, 'next'); await el.updateComplete; @@ -73,7 +75,7 @@ describe('', () => { Node 3 `); - sinon.stub(el, 'next'); + sandbox.stub(el, 'next'); await el.updateComplete; @@ -96,7 +98,7 @@ describe('', () => { Node 3 `); - sinon.stub(el, 'next'); + sandbox.stub(el, 'next'); await el.updateComplete; @@ -183,7 +185,7 @@ describe('', () => { Node 3 `); - sinon.stub(el, 'goToSlide'); + sandbox.stub(el, 'goToSlide'); await el.updateComplete; // Act @@ -473,7 +475,7 @@ describe('', () => { `); const nextButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button--next')!; - sinon.stub(el, 'next'); + sandbox.stub(el, 'next'); await el.updateComplete; @@ -496,7 +498,7 @@ describe('', () => { `); 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'); @@ -560,7 +562,7 @@ describe('', () => { await el.updateComplete; const previousButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button--previous')!; - sinon.stub(el, 'previous'); + sandbox.stub(el, 'previous'); await el.updateComplete; @@ -584,7 +586,7 @@ describe('', () => { `); const previousButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button--previous')!; - sinon.stub(el, 'previous'); + sandbox.stub(el, 'previous'); await el.updateComplete; // Act @@ -632,19 +634,27 @@ describe('', () => { it('should scroll the carousel to the next slide', async () => { // Arrange const el = await fixture(html` - + Node 1 Node 2 Node 3 `); - 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); }); }); @@ -652,19 +662,32 @@ describe('', () => { it('should scroll the carousel to the previous slide', async () => { // Arrange const el = await fixture(html` - + Node 1 Node 2 Node 3 `); - 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 nextFrame(); + + sandbox.spy(el, 'goToSlide'); // Act el.previous(); + await oneEvent(el.scrollContainer, 'scrollend'); - 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); }); }); diff --git a/src/components/checkbox/checkbox.component.ts b/src/components/checkbox/checkbox.component.ts index bb37a5a4..903d13f0 100644 --- a/src/components/checkbox/checkbox.component.ts +++ b/src/components/checkbox/checkbox.component.ts @@ -1,11 +1,13 @@ 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 ShoelaceElement from '../../internal/shoelace-element.js'; import SlIcon from '../icon/icon.component.js'; import styles from './checkbox.styles.js'; @@ -21,6 +23,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 +38,10 @@ import type { ShoelaceFormControl } from '../../internal/shoelace-element.js'; * @csspart checked-icon - The checked icon, an `` element. * @csspart indeterminate-icon - The indeterminate icon, an `` 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, styles]; static dependencies = { 'sl-icon': SlIcon }; private readonly formControlController = new FormControlController(this, { @@ -45,6 +49,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 +91,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 +186,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
around the label slot because of this Chrome bug. // // https://bugs.chromium.org/p/chromium/issues/detail?id=1413733 // return html` -