From 9f8ce582888a6a52c2ca4186fa7e80c8eaa44af9 Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Tue, 28 Feb 2023 13:07:07 -0500 Subject: [PATCH 01/12] use clickOnElement --- src/components/tree/tree.test.ts | 41 ++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/src/components/tree/tree.test.ts b/src/components/tree/tree.test.ts index b1660289..2805c9ca 100644 --- a/src/components/tree/tree.test.ts +++ b/src/components/tree/tree.test.ts @@ -1,5 +1,7 @@ -import { expect, fixture, html, triggerBlurFor, triggerFocusFor } from '@open-wc/testing'; +import { aTimeout, expect, fixture, html, triggerBlurFor, triggerFocusFor } from '@open-wc/testing'; +import { clickOnElement } from '../../internal/test'; import { sendKeys } from '@web/test-runner-commands'; +import { waitForEvent } from '../../internal/event'; import sinon from 'sinon'; import type SlTree from './tree'; import type SlTreeItem from '../tree-item/tree-item'; @@ -433,7 +435,7 @@ describe('', () => { const expandButton: HTMLElement = node.shadowRoot!.querySelector('.tree-item__expand-button')!; // Act - expandButton.click(); + await clickOnElement(expandButton); await el.updateComplete; // Assert @@ -453,10 +455,10 @@ describe('', () => { await el.updateComplete; // Act - node0.click(); + await clickOnElement(node0); await el.updateComplete; - node1.click(); + await clickOnElement(node1); await el.updateComplete; // Assert @@ -474,10 +476,10 @@ describe('', () => { await el.updateComplete; // Act - node0.click(); + await clickOnElement(node0); await el.updateComplete; - node1.click(); + await clickOnElement(node1); await el.updateComplete; // Assert @@ -492,7 +494,7 @@ describe('', () => { await el.updateComplete; // Act - parentNode.click(); + await clickOnElement(parentNode); await parentNode.updateComplete; // Assert @@ -511,10 +513,10 @@ describe('', () => { await el.updateComplete; // Act - node0.click(); + await clickOnElement(node0); await el.updateComplete; - node1.click(); + await clickOnElement(node1); await el.updateComplete; // Assert @@ -529,7 +531,7 @@ describe('', () => { const parentNode = el.children[2] as SlTreeItem; // Act - parentNode.click(); + await clickOnElement(parentNode); await el.updateComplete; // Assert @@ -549,7 +551,10 @@ describe('', () => { const childNode = parentNode.children[0] as SlTreeItem; // Act - childNode.click(); + parentNode.expanded = true; + await parentNode.updateComplete; + await aTimeout(300); + await clickOnElement(childNode); await el.updateComplete; // Assert @@ -572,9 +577,9 @@ describe('', () => { const node = el.children[0] as SlTreeItem; // Act - node.click(); + await clickOnElement(node); await el.updateComplete; - node.click(); + await clickOnElement(node); await Promise.all([node.updateComplete, el.updateComplete]); // Assert @@ -598,9 +603,9 @@ describe('', () => { const node = el.children[0] as SlTreeItem; // Act - node.click(); + await clickOnElement(node); await el.updateComplete; - node.click(); + await clickOnElement(node); await Promise.all([node.updateComplete, el.updateComplete]); // Assert @@ -621,7 +626,7 @@ describe('', () => { const node = el.querySelector('#expandable')!; // Act - node.click(); + await clickOnElement(node); await Promise.all([node.updateComplete, el.updateComplete]); // Assert @@ -643,9 +648,9 @@ describe('', () => { const node = el.children[0] as SlTreeItem; // Act - node.click(); + await clickOnElement(node); await Promise.all([node.updateComplete, el.updateComplete]); - node.click(); + await clickOnElement(node); await Promise.all([node.updateComplete, el.updateComplete]); // Assert From d79799043ac4bbedff37e537c278c3ac2f60e2d6 Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Tue, 28 Feb 2023 13:07:18 -0500 Subject: [PATCH 02/12] remove unused import --- src/components/tree/tree.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/tree/tree.test.ts b/src/components/tree/tree.test.ts index 2805c9ca..233c5474 100644 --- a/src/components/tree/tree.test.ts +++ b/src/components/tree/tree.test.ts @@ -1,7 +1,6 @@ import { aTimeout, expect, fixture, html, triggerBlurFor, triggerFocusFor } from '@open-wc/testing'; import { clickOnElement } from '../../internal/test'; import { sendKeys } from '@web/test-runner-commands'; -import { waitForEvent } from '../../internal/event'; import sinon from 'sinon'; import type SlTree from './tree'; import type SlTreeItem from '../tree-item/tree-item'; From 3ea31389ddb4da5d1412b6d591d40b93842e2f56 Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Tue, 28 Feb 2023 13:33:34 -0500 Subject: [PATCH 03/12] fixes #1082 --- docs/resources/changelog.md | 1 + src/components/tree/tree.ts | 25 ++++++++++++++++++++++--- src/internal/test.ts | 1 + 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md index 982d5e27..cf710c7a 100644 --- a/docs/resources/changelog.md +++ b/docs/resources/changelog.md @@ -20,6 +20,7 @@ New versions of Shoelace are released as-needed and generally occur when a criti - Fixed a bug in `` that caused the focus color to show when selecting menu items with a mouse or touch device - Fixed a bug in `` that caused `sl-change` and `sl-input` to be emitted too early [#1201](https://github.com/shoelace-style/shoelace/issues/1201) - Fixed a positioning edge case that caused `` to positioned nested popups incorrectly [#1135](https://github.com/shoelace-style/shoelace/issues/1135) +- Fixed a bug in `` that caused the tree item to collapse when clicking a child item, dragging the mouse, and releasing it on the parent node [#1082](https://github.com/shoelace-style/shoelace/issues/1082) - Updated `@shoelace-style/localize` to 3.1.0 - Updated `@floating-ui/dom` to 1.2.1 diff --git a/src/components/tree/tree.ts b/src/components/tree/tree.ts index 60878355..22f9c3eb 100644 --- a/src/components/tree/tree.ts +++ b/src/components/tree/tree.ts @@ -90,6 +90,7 @@ export default class SlTree extends ShoelaceElement { private lastFocusedItem: SlTreeItem; private readonly localize = new LocalizeController(this); private mutationObserver: MutationObserver; + private clickTarget: SlTreeItem | null = null; async connectedCallback() { super.connectedCallback(); @@ -292,13 +293,20 @@ export default class SlTree extends ShoelaceElement { } private handleClick(event: Event) { - const target = event.target as HTMLElement; + const target = event.target as SlTreeItem; const treeItem = target.closest('sl-tree-item')!; const isExpandButton = event .composedPath() .some((el: HTMLElement) => el?.classList?.contains('tree-item__expand-button')); - if (!treeItem || treeItem.disabled) { + // + // Don't Do anything if there's no tree item, if it's disabled, or if the click doesn't match the initial target + // from mousedown. The latter case prevents the user from starting a click on one item and ending it on another, + // causing the parent node to collapse. + // + // See https://github.com/shoelace-style/shoelace/issues/1082 + // + if (!treeItem || treeItem.disabled || target !== this.clickTarget) { return; } @@ -309,6 +317,11 @@ export default class SlTree extends ShoelaceElement { } } + handleMouseDown(event: MouseEvent) { + // Record the click target so we know which item the click initially targeted + this.clickTarget = event.target as SlTreeItem; + } + private handleFocusOut(event: FocusEvent) { const relatedTarget = event.relatedTarget as HTMLElement; @@ -392,7 +405,13 @@ export default class SlTree extends ShoelaceElement { render() { return html` -
+
diff --git a/src/internal/test.ts b/src/internal/test.ts index e4dc703c..a1f10dad 100644 --- a/src/internal/test.ts +++ b/src/internal/test.ts @@ -50,6 +50,7 @@ export async function clickOnElement( await sendMouse({ type: 'click', position: [clickX, clickY] }); } +/** A testing utility that moves the mouse onto an element. */ export async function moveMouseOnElement( /** The element to click */ el: Element, From 954d78dcd169728971224a51a94691c4c4c07002 Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Tue, 28 Feb 2023 17:04:59 -0500 Subject: [PATCH 04/12] update version --- docs/resources/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md index cf710c7a..984e1eba 100644 --- a/docs/resources/changelog.md +++ b/docs/resources/changelog.md @@ -6,7 +6,7 @@ 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). -## Next +## 2.2.0 - Added TypeScript types to all custom events [#1183](https://github.com/shoelace-style/shoelace/pull/1183) - Added the `svg` part to `` From 5990fbd0000312a2e4224d9d79a81dce1058d3d5 Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Tue, 28 Feb 2023 17:05:11 -0500 Subject: [PATCH 05/12] 2.2.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 57c3f7f8..21a199e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@shoelace-style/shoelace", - "version": "2.1.0", + "version": "2.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@shoelace-style/shoelace", - "version": "2.1.0", + "version": "2.2.0", "license": "MIT", "dependencies": { "@ctrl/tinycolor": "^3.5.0", diff --git a/package.json b/package.json index 0cf9da79..e0fd47f2 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.1.0", + "version": "2.2.0", "homepage": "https://github.com/shoelace-style/shoelace", "author": "Cory LaViska", "license": "MIT", From a8d59b3329446e6e83ce7f806196067278ebc715 Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Tue, 28 Feb 2023 17:11:21 -0500 Subject: [PATCH 06/12] update changelog --- docs/resources/changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md index 984e1eba..e7bf7d6e 100644 --- a/docs/resources/changelog.md +++ b/docs/resources/changelog.md @@ -11,6 +11,7 @@ New versions of Shoelace are released as-needed and generally occur when a criti - Added TypeScript types to all custom events [#1183](https://github.com/shoelace-style/shoelace/pull/1183) - Added the `svg` part to `` - Added the `getForm()` method to all form controls [#1180](https://github.com/shoelace-style/shoelace/issues/1180) +- Added the experimental carousel component [#851](https://github.com/shoelace-style/shoelace/pull/851) - Fixed a bug in `` that caused the display label to render incorrectly in Chrome after form validation [#1197](https://github.com/shoelace-style/shoelace/discussions/1197) - Fixed a bug in `` that prevented users from applying their own value for `autocapitalize`, `autocomplete`, and `autocorrect` when using `type="password` [#1205](https://github.com/shoelace-style/shoelace/issues/1205) - Fixed a bug in `` that prevented scroll controls from showing when dynamically adding tabs [#1208](https://github.com/shoelace-style/shoelace/issues/1208) From 77b25f45814139ef887fbda06965dab0962e0ee1 Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Wed, 1 Mar 2023 10:58:24 -0500 Subject: [PATCH 07/12] add tag parts to --- docs/resources/changelog.md | 4 ++++ src/components/select/select.ts | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md index e7bf7d6e..5a25101d 100644 --- a/docs/resources/changelog.md +++ b/docs/resources/changelog.md @@ -6,6 +6,10 @@ 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). +## Next + +- Added `tag__base`, `tag__content`, `tag__remove-button`, `tag__remove-button__base` parts to `` + ## 2.2.0 - Added TypeScript types to all custom events [#1183](https://github.com/shoelace-style/shoelace/pull/1183) diff --git a/src/components/select/select.ts b/src/components/select/select.ts index e152d056..deef53de 100644 --- a/src/components/select/select.ts +++ b/src/components/select/select.ts @@ -59,6 +59,10 @@ import type SlRemoveEvent from '../../events/sl-remove'; * @csspart listbox - The listbox container where options are slotted. * @csspart tags - The container that houses option tags when `multiselect` is used. * @csspart tag - The individual tags that represent each multiselect option. + * @csspart tag__base - The tag's base part. + * @csspart tag__content - The tag's content part. + * @csspart tag__remove-button - The tag's remove button. + * @csspart tag__remove-button__base - The tag's remove button base part. * @csspart clear-button - The clear button. * @csspart expand-icon - The container that wraps the expand icon. */ @@ -761,6 +765,12 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon return html` Date: Wed, 1 Mar 2023 17:05:29 +0100 Subject: [PATCH 08/12] fix(carousel): various fixes and improvements (#1216) * fix(carousel): don't resume autoplay if interacting * fix(carousel): wrap pagination items * chore(carousel): add unit tests * feat(carousel): more reactive pagination dots * fix(carousel): trigger scrollend when user scroll exactly over a snap point --- .../carousel/autoplay-controller.ts | 13 ++++-- src/components/carousel/carousel.styles.ts | 1 + src/components/carousel/carousel.test.ts | 28 ++++++++++++ src/components/carousel/carousel.ts | 43 ++++++++++++------- src/components/carousel/scroll-controller.ts | 2 + 5 files changed, 68 insertions(+), 19 deletions(-) diff --git a/src/components/carousel/autoplay-controller.ts b/src/components/carousel/autoplay-controller.ts index 4cd97879..7fc6adda 100644 --- a/src/components/carousel/autoplay-controller.ts +++ b/src/components/carousel/autoplay-controller.ts @@ -8,6 +8,7 @@ export class AutoplayController implements ReactiveController { private host: ReactiveElement; private timerId = 0; private tickCallback: () => void; + private activeInteractions = 0; paused = false; stopped = true; @@ -57,12 +58,16 @@ export class AutoplayController implements ReactiveController { } pause = () => { - this.paused = true; - this.host.requestUpdate(); + if (!this.activeInteractions++) { + this.paused = true; + this.host.requestUpdate(); + } }; resume = () => { - this.paused = false; - this.host.requestUpdate(); + if (!--this.activeInteractions) { + this.paused = false; + this.host.requestUpdate(); + } }; } diff --git a/src/components/carousel/carousel.styles.ts b/src/components/carousel/carousel.styles.ts index d6b31cac..a4488308 100644 --- a/src/components/carousel/carousel.styles.ts +++ b/src/components/carousel/carousel.styles.ts @@ -30,6 +30,7 @@ export default css` grid-area: pagination; display: flex; + flex-wrap: wrap; justify-content: center; gap: var(--sl-spacing-small); } diff --git a/src/components/carousel/carousel.test.ts b/src/components/carousel/carousel.test.ts index 1f6b36c1..1f4ca4c8 100644 --- a/src/components/carousel/carousel.test.ts +++ b/src/components/carousel/carousel.test.ts @@ -78,6 +78,34 @@ describe('', () => { // Assert expect(el.next).not.to.have.been.called; }); + + it('should not resume if the user is still interacting', async () => { + // Arrange + const el = await fixture(html` + + Node 1 + Node 2 + Node 3 + + `); + sinon.stub(el, 'next'); + + await el.updateComplete; + + // Act + el.dispatchEvent(new Event('mouseenter')); + el.dispatchEvent(new Event('focusin')); + await el.updateComplete; + + el.dispatchEvent(new Event('mouseleave')); + await el.updateComplete; + + clock.next(); + clock.next(); + + // Assert + expect(el.next).not.to.have.been.called; + }); }); describe('when `loop` attribute is provided', () => { diff --git a/src/components/carousel/carousel.ts b/src/components/carousel/carousel.ts index ab31cff9..417e8ce5 100644 --- a/src/components/carousel/carousel.ts +++ b/src/components/carousel/carousel.ts @@ -168,13 +168,21 @@ export default class SlCarousel extends ShoelaceElement { goToSlide(index: number, behavior: ScrollBehavior = 'smooth') { const { slidesPerPage, loop } = this; + const slides = this.getSlides(); const slidesWithClones = this.getSlides({ excludeClones: false }); - const normalizedIndex = clamp(index + (loop ? slidesPerPage : 0), 0, slidesWithClones.length - 1); - const slide = slidesWithClones[normalizedIndex]; + + // Sets the next index without taking into account clones, if any. + const newActiveSlide = (index + slides.length) % slides.length; + this.activeSlide = newActiveSlide; + + // Get the index of the next slide. For looping carousel it adds `slidesPerPage` + // to normalize the starting index in order to ignore the first nth clones. + const nextSlideIndex = clamp(index + (loop ? slidesPerPage : 0), 0, slidesWithClones.length - 1); + const nextSlide = slidesWithClones[nextSlideIndex]; this.scrollContainer.scrollTo({ - left: slide.offsetLeft, - top: slide.offsetTop, + left: nextSlide.offsetLeft, + top: nextSlide.offsetTop, behavior: prefersReducedMotion() ? 'auto' : behavior }); } @@ -307,13 +315,18 @@ export default class SlCarousel extends ShoelaceElement { this.scrollController.mouseDragging = this.mouseDragging; } - private renderPagination = () => { - const slides = this.getSlides(); - const slidesCount = slides.length; + private getPageCount() { + return Math.ceil(this.getSlides().length / this.slidesPerPage); + } - const { activeSlide, slidesPerPage } = this; - const pagesCount = Math.ceil(slidesCount / slidesPerPage); - const currentPage = Math.floor(activeSlide / slidesPerPage); + private getCurrentPage() { + return Math.floor(this.activeSlide / this.slidesPerPage); + } + + private renderPagination = () => { + const { slidesPerPage } = this; + const pagesCount = this.getPageCount(); + const currentPage = this.getCurrentPage(); return html`