mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-12 04:09:12 +00:00
Merge branch 'next' into autoload
This commit is contained in:
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -77,6 +77,8 @@ export class ScrollController<T extends ScrollHost> implements ReactiveControlle
|
||||
})
|
||||
);
|
||||
this.host.requestUpdate();
|
||||
} else {
|
||||
this.handleScrollEnd();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user