kopia lustrzana https://github.com/shoelace-style/shoelace
Merge branch 'next' into autoload
commit
a27fd4d2e9
|
@ -582,12 +582,19 @@ The content of the carousel can be changed by appending or removing carousel ite
|
|||
slide.innerText = `Slide ${dynamicCarousel.children.length + 1}`;
|
||||
slide.style.setProperty('background', `var(--sl-color-${color}-200)`);
|
||||
dynamicCarousel.appendChild(slide);
|
||||
dynamicRemove.disabled = false;
|
||||
};
|
||||
|
||||
const removeSlide = () => {
|
||||
const slide = dynamicCarousel.children[dynamicCarousel.children.length - 1];
|
||||
slide.remove();
|
||||
colorIndex--;
|
||||
const numSlides = dynamicCarousel.querySelectorAll('sl-carousel-item').length;
|
||||
|
||||
if (numSlides > 1) {
|
||||
slide.remove();
|
||||
colorIndex--;
|
||||
}
|
||||
|
||||
dynamicRemove.disabled = numSlides - 1 <= 1;
|
||||
};
|
||||
|
||||
dynamicAdd.addEventListener('click', addSlide);
|
||||
|
|
|
@ -10,9 +10,14 @@ New versions of Shoelace are released as-needed and generally occur when a criti
|
|||
|
||||
- Added an experimental autoloader
|
||||
- Added the `subpath` argument to `getBasePath()` to make it easier to generate full paths to any file
|
||||
- Added `tag__base`, `tag__content`, `tag__remove-button`, `tag__remove-button__base` parts to `<sl-select>`
|
||||
|
||||
## 2.2.0
|
||||
|
||||
- Added TypeScript types to all custom events [#1183](https://github.com/shoelace-style/shoelace/pull/1183)
|
||||
- Added the `svg` part to `<sl-icon>`
|
||||
- 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 `<sl-select>` 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 `<sl-input>` 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 `<sl-tab-group>` that prevented scroll controls from showing when dynamically adding tabs [#1208](https://github.com/shoelace-style/shoelace/issues/1208)
|
||||
|
@ -22,6 +27,7 @@ New versions of Shoelace are released as-needed and generally occur when a criti
|
|||
- Fixed a bug in `<sl-menu-item>` that caused the focus color to show when selecting menu items with a mouse or touch device
|
||||
- Fixed a bug in `<sl-select>` 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 `<sl-popup>` to positioned nested popups incorrectly [#1135](https://github.com/shoelace-style/shoelace/issues/1135)
|
||||
- Fixed a bug in `<sl-tree>` 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
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
Ładowanie…
Reference in New Issue