Merge branch 'next' into autoload

This commit is contained in:
Cory LaViska
2023-03-02 10:49:30 -05:00
13 changed files with 145 additions and 46 deletions

View File

@@ -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();
}
};
}

View File

@@ -28,8 +28,8 @@ export default css`
.carousel__pagination {
grid-area: pagination;
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: var(--sl-spacing-small);
}
@@ -64,6 +64,7 @@ export default css`
scroll-snap-type: x mandatory;
scroll-padding-inline: var(--scroll-hint);
padding-inline: var(--scroll-hint);
overflow-y: hidden;
}
.carousel__slides--vertical {
@@ -74,6 +75,7 @@ export default css`
scroll-snap-type: y mandatory;
scroll-padding-block: var(--scroll-hint);
padding-block: var(--scroll-hint);
overflow-x: hidden;
}
.carousel__slides--dragging,
@@ -140,6 +142,8 @@ export default css`
background-color: var(--sl-color-neutral-300);
will-change: transform;
transition: var(--sl-transition-fast) ease-in;
padding: 0;
margin: 0;
}
.carousel__pagination-item--active {

View File

@@ -78,6 +78,34 @@ describe('<sl-carousel>', () => {
// 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<SlCarousel>(html`
<sl-carousel autoplay autoplay-interval="10">
<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, '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', () => {

View File

@@ -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`
<nav part="pagination" role="tablist" class="carousel__pagination" aria-controls="scroll-container">
@@ -338,11 +351,11 @@ export default class SlCarousel extends ShoelaceElement {
};
private renderNavigation = () => {
const { loop, activeSlide } = this;
const slides = this.getSlides();
const slidesCount = slides.length;
const prevEnabled = loop || activeSlide > 0;
const nextEnabled = loop || activeSlide < slidesCount - 1;
const { loop } = this;
const pagesCount = this.getPageCount();
const currentPage = this.getCurrentPage();
const prevEnabled = loop || currentPage > 0;
const nextEnabled = loop || currentPage < pagesCount - 1;
const isLtr = this.localize.dir() === 'ltr';
return html`

View File

@@ -77,6 +77,8 @@ export class ScrollController<T extends ScrollHost> implements ReactiveControlle
})
);
this.host.requestUpdate();
} else {
this.handleScrollEnd();
}
}

View File

@@ -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.
*/
@@ -766,6 +770,12 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
return html`
<sl-tag
part="tag"
exportparts="
base:tag__base,
content:tag__content,
remove-button:tag__remove-button,
remove-button__base:tag__remove-button__base
"
?pill=${this.pill}
size=${this.size}
removable

View File

@@ -1,4 +1,5 @@
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 sinon from 'sinon';
import type SlTree from './tree';
@@ -433,7 +434,7 @@ describe('<sl-tree>', () => {
const expandButton: HTMLElement = node.shadowRoot!.querySelector('.tree-item__expand-button')!;
// Act
expandButton.click();
await clickOnElement(expandButton);
await el.updateComplete;
// Assert
@@ -453,10 +454,10 @@ describe('<sl-tree>', () => {
await el.updateComplete;
// Act
node0.click();
await clickOnElement(node0);
await el.updateComplete;
node1.click();
await clickOnElement(node1);
await el.updateComplete;
// Assert
@@ -474,10 +475,10 @@ describe('<sl-tree>', () => {
await el.updateComplete;
// Act
node0.click();
await clickOnElement(node0);
await el.updateComplete;
node1.click();
await clickOnElement(node1);
await el.updateComplete;
// Assert
@@ -492,7 +493,7 @@ describe('<sl-tree>', () => {
await el.updateComplete;
// Act
parentNode.click();
await clickOnElement(parentNode);
await parentNode.updateComplete;
// Assert
@@ -511,10 +512,10 @@ describe('<sl-tree>', () => {
await el.updateComplete;
// Act
node0.click();
await clickOnElement(node0);
await el.updateComplete;
node1.click();
await clickOnElement(node1);
await el.updateComplete;
// Assert
@@ -529,7 +530,7 @@ describe('<sl-tree>', () => {
const parentNode = el.children[2] as SlTreeItem;
// Act
parentNode.click();
await clickOnElement(parentNode);
await el.updateComplete;
// Assert
@@ -549,7 +550,10 @@ describe('<sl-tree>', () => {
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 +576,9 @@ describe('<sl-tree>', () => {
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 +602,9 @@ describe('<sl-tree>', () => {
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 +625,7 @@ describe('<sl-tree>', () => {
const node = el.querySelector<SlTreeItem>('#expandable')!;
// Act
node.click();
await clickOnElement(node);
await Promise.all([node.updateComplete, el.updateComplete]);
// Assert
@@ -643,9 +647,9 @@ describe('<sl-tree>', () => {
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

View File

@@ -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`
<div part="base" class="tree" @click=${this.handleClick} @keydown=${this.handleKeyDown}>
<div
part="base"
class="tree"
@click=${this.handleClick}
@keydown=${this.handleKeyDown}
@mousedown=${this.handleMouseDown}
>
<slot @slotchange=${this.handleSlotChange}></slot>
<slot name="expand-icon" hidden aria-hidden="true"> </slot>
<slot name="collapse-icon" hidden aria-hidden="true"> </slot>

View File

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