Porównaj commity

...

13 Commity

Autor SHA1 Wiadomość Data
konnorrogers d0204c2297 prettier 2024-03-25 13:19:57 -04:00
konnorrogers f10282f1e6 use data attributes 2024-03-25 12:28:33 -04:00
Cory LaViska 0b7e70bccf update changelog 2024-03-25 11:23:37 -04:00
Alessandro 31f2600816
fix(carousel): synchronize slides after scroll (#1923)
* fix(carousel): synchronize slides after scroll

* chore: leftovers
2024-03-25 11:22:28 -04:00
Cory LaViska f6d5344c44 update changelog 2024-03-25 11:21:32 -04:00
Nic Newdigate 5a89439e14
dropdown: add optional sync property to align popup width to trigger slot element width (#1935)
Co-authored-by: Nic Newdigate <nic.newdigate@vivantio.com>
2024-03-25 11:20:23 -04:00
Cory LaViska 2878957ef5
fix clear button clicks (#1911) 2024-03-25 11:16:32 -04:00
Konnor Rogers cd5a6486da
fix(tree) icons rendering as null (#1922)
* fix icons rendering as null

* prettier

* prettier

* Update docs/pages/resources/changelog.md

---------

Co-authored-by: Cory LaViska <cory@abeautifulsite.net>
2024-03-25 11:15:51 -04:00
Cory LaViska 77d6f27248 update changelog 2024-03-25 11:15:03 -04:00
Konnor Rogers 7a62a87b9b
apply mutator to spritesheets (#1927)
* apply mutator to spritesheets

* prettier

* Update docs/pages/resources/changelog.md

---------

Co-authored-by: Cory LaViska <cory@abeautifulsite.net>
2024-03-25 11:14:04 -04:00
Cory LaViska 0ac61a6a22 prettier 2024-03-25 11:13:01 -04:00
Matt Walkland acf76cf359
Expose spinner part on tree item (#1937)
* Expose spinner part on tree item

* Add spinner__base to exportparts of tree-item
2024-03-25 10:59:50 -04:00
Cory LaViska 3451ec753c add ks banner 2024-03-22 11:44:22 -04:00
15 zmienionych plików z 292 dodań i 101 usunięć

Wyświetl plik

@ -95,6 +95,23 @@
</sl-dropdown>
</div>
<a
class="ks-banner{% if toc %} with-toc{% endif %}"
href="https://www.kickstarter.com/projects/fontawesome/web-awesome?ref=71ihfk"
target="_blank"
>
<span>
<svg viewBox="0 0 20 16" xmlns="http://www.w3.org/2000/svg">
<path fill="#f36944" d="M11.63 1.625C11.63 2.27911 11.2435 2.84296 10.6865 3.10064L14 6L17.2622 5.34755C17.0968 5.10642 17 4.81452 17 4.5C17 3.67157 17.6716 3 18.5 3C19.3284 3 20 3.67157 20 4.5C20 5.31157 19.3555 5.9726 18.5504 5.99917L15.0307 13.8207C14.7077 14.5384 13.9939 15 13.2068 15H6.79317C6.00615 15 5.29229 14.5384 4.96933 13.8207L1.44963 5.99917C0.64452 5.9726 0 5.31157 0 4.5C0 3.67157 0.671573 3 1.5 3C2.32843 3 3 3.67157 3 4.5C3 4.81452 2.9032 5.10642 2.73777 5.34755L6 6L9.31702 3.09761C8.76346 2.83855 8.38 2.27656 8.38 1.625C8.38 0.727537 9.10754 0 10.005 0C10.9025 0 11.63 0.727537 11.63 1.625Z"/>
</svg>
<span>
<strong style="white-space: nowrap;">Get ready for more awesome!</strong>
Web Awesome, the next iteration of Shoelace, is on Kickstarter.
</span>
</span>
<span class="faux-button">Read Our Story</span>
</a>
<aside id="sidebar" data-preserve-scroll>
<header>
<a href="/">

Wyświetl plik

@ -1413,3 +1413,95 @@ body[data-page^='/tokens/'] .table-wrapper td:first-child code {
grid-column-start: span 6;
}
}
.ks-banner {
display: flex;
gap: 1rem;
position: absolute;
top: 1rem;
width: 950px;
left: calc(50% - 475px);
font-size: 0.9375rem;
align-items: center;
justify-content: space-between;
background: #1a3256;
border-radius: var(--sl-border-radius-large);
padding: 1rem 1.25rem;
color: #fdfdfd;
text-decoration: none;
line-height: 1.4;
z-index: 2;
margin-left: 160px;
}
.ks-banner:hover {
color: #fdfdfd;
}
.ks-banner > span {
display: flex;
align-items: center;
gap: 1rem;
}
.ks-banner svg {
flex: 0 0 1.5rem;
width: 1.5rem;
height: 1.5rem;
}
.ks-banner .faux-button {
display: inline-flex;
align-items: center;
height: 30px;
background: white;
border: solid 1px #d4d4d4;
border-radius: var(--sl-border-radius-medium);
font-size: 0.8375rem;
color: #353439;
padding: 0.5rem 1rem;
white-space: nowrap;
}
.ks-banner.with-toc {
width: 1100px;
left: calc(50% - 550px);
margin-left: 140px;
}
main {
margin-top: 70px;
}
@media screen and (max-width: 1650px) {
.ks-banner,
.ks-banner.with-toc {
width: 540px !important;
top: 50px;
left: calc(50% - 270px);
}
main {
margin-top: 140px;
}
}
@media screen and (max-width: 900px) {
.ks-banner,
.ks-banner.with-toc {
margin-left: 0;
}
}
@media screen and (max-width: 680px) {
.ks-banner,
.ks-banner.with-toc {
width: calc(100% - 2rem) !important;
left: 1rem;
flex-direction: column;
}
main {
margin-top: 150px;
}
}

Wyświetl plik

@ -16,7 +16,10 @@ New versions of Shoelace are released as-needed and generally occur when a criti
- Added the Slovenian translation [#1893]
- Added support for `contextElement` to `VirtualElements` in `<sl-popup>` [#1874]
- Fixed a bug in `.sl-scroll-lock` causing layout shifts. [#1895]
- Added the `spinner` and `spinner__base` parts to `<sl-tree-item>` [#1937]
- Added the `sync` property to `<sl-dropdown>` so the menu can easily sync sizes with the trigger element [#1935]
- Fixed a bug in `<sl-icon>` that did not properly apply mutators to spritesheets [#1927]
- Fixed a bug in `.sl-scroll-lock` causing layout shifts [#1895]
- Fixed a bug in `<sl-rating>` that caused the rating to not reset in some circumstances [#1877]
- Fixed a bug in `<sl-select>` that caused the menu to not close when rendered in a shadow root [#1878]
- Fixed a bug in `<sl-tree>` that caused a new stacking context resulting in tooltips being clipped [#1709]
@ -25,6 +28,8 @@ New versions of Shoelace are released as-needed and generally occur when a criti
- Fixed a bug in `<sl-select>` where the tag size wouldn't update with the control's size [#1886]
- Fixed a bug in `<sl-checkbox>` and `<sl-switch>` where the color of the required content wasn't applying correctly
- Fixed a bug in `<sl-checkbox>` where help text was incorrectly styled [#1897]
- Fixed a bug in `<sl-input>` that prevented the control from receiving focus when clicking over the clear button
- Fixed a bug in `<sl-carousel>` that caused the carousel to be out of sync when used with reduced motion settings [#1887]
## 2.14.0
@ -44,6 +49,7 @@ New versions of Shoelace are released as-needed and generally occur when a criti
- Added the `hover-bridge` feature to `<sl-popup>` to support better tooltip accessibility [#1734]
- Added the `loading` attribute and the `spinner` and `spinner__base` part to `<sl-menu-item>` [#1700]
- Fixed files that did not have `.js` extensions. [#1770]
- Fixed a bug in `<sl-tree>` when providing custom expand / collapse icons [#1922]
- Fixed `<sl-dialog>` not accounting for elements with hidden dialog controls like `<video>` [#1755]
- Fixed focus trapping not scrolling elements into view. [#1750]
- Fixed more performance issues with focus trapping performance. [#1750]

Wyświetl plik

@ -30,22 +30,22 @@ export default class SlButtonGroup extends ShoelaceElement {
private handleFocus(event: Event) {
const button = findButton(event.target as HTMLElement);
button?.classList.add('sl-button-group__button--focus');
button?.toggleAttribute('data-sl-button-group__button--focus', true);
}
private handleBlur(event: Event) {
const button = findButton(event.target as HTMLElement);
button?.classList.remove('sl-button-group__button--focus');
button?.toggleAttribute('data-sl-button-group__button--focus', false);
}
private handleMouseOver(event: Event) {
const button = findButton(event.target as HTMLElement);
button?.classList.add('sl-button-group__button--hover');
button?.toggleAttribute('data-sl-button-group__button--hover', true);
}
private handleMouseOut(event: Event) {
const button = findButton(event.target as HTMLElement);
button?.classList.remove('sl-button-group__button--hover');
button?.toggleAttribute('data-sl-button-group__button--hover', true);
}
private handleSlotChange() {
@ -56,11 +56,14 @@ export default class SlButtonGroup extends ShoelaceElement {
const button = findButton(el);
if (button) {
button.classList.add('sl-button-group__button');
button.classList.toggle('sl-button-group__button--first', index === 0);
button.classList.toggle('sl-button-group__button--inner', index > 0 && index < slottedElements.length - 1);
button.classList.toggle('sl-button-group__button--last', index === slottedElements.length - 1);
button.classList.toggle('sl-button-group__button--radio', button.tagName.toLowerCase() === 'sl-radio-button');
button.toggleAttribute('data-sl-button-group__button', true);
button.toggleAttribute('data-sl-button-group__button--first', index === 0);
button.toggleAttribute('data-sl-button-group__button--inner', index > 0 && index < slottedElements.length - 1);
button.toggleAttribute('data-sl-button-group__button--last', index === slottedElements.length - 1);
button.toggleAttribute(
'data-sl-button-group__button--radio',
button.tagName.toLowerCase() === 'sl-radio-button'
);
}
});
}

Wyświetl plik

@ -546,30 +546,30 @@ export default css`
* buttons and we style them here instead.
*/
:host(.sl-button-group__button--first:not(.sl-button-group__button--last)) .button {
:host([data-sl-button-group__button--first]:not([data-sl-button-group__button--last])) .button {
border-start-end-radius: 0;
border-end-end-radius: 0;
}
:host(.sl-button-group__button--inner) .button {
:host([data-sl-button-group__button--inner]) .button {
border-radius: 0;
}
:host(.sl-button-group__button--last:not(.sl-button-group__button--first)) .button {
:host([data-sl-button-group__button--last]:not([data-sl-button-group__button--first])) .button {
border-start-start-radius: 0;
border-end-start-radius: 0;
}
/* All except the first */
:host(.sl-button-group__button:not(.sl-button-group__button--first)) {
:host([data-sl-button-group__button]:not([data-sl-button-group__button--first])) {
margin-inline-start: calc(-1 * var(--sl-input-border-width));
}
/* Add a visual separator between solid buttons */
:host(
.sl-button-group__button:not(
.sl-button-group__button--first,
.sl-button-group__button--radio,
[data-sl-button-group__button]:not(
[data-sl-button-group__button--first],
[data-sl-button-group__button--radio],
[variant='default']
):not(:hover)
)
@ -584,13 +584,13 @@ export default css`
}
/* Bump hovered, focused, and checked buttons up so their focus ring isn't clipped */
:host(.sl-button-group__button--hover) {
:host([data-sl-button-group__button--hover]) {
z-index: 1;
}
/* Focus and checked are always on top */
:host(.sl-button-group__button--focus),
:host(.sl-button-group__button[checked]) {
:host([data-sl-button-group__button--focus]),
:host([data-sl-button-group__button[checked]]) {
z-index: 2;
}
`;

Wyświetl plik

@ -92,9 +92,6 @@ export default class SlCarousel extends ShoelaceElement {
@state() dragging = false;
private autoplayController = new AutoplayController(this, () => this.next());
private intersectionObserver: IntersectionObserver; // determines which slide is displayed
// A map containing the state of all the slides
private readonly intersectionObserverEntries = new Map<Element, IntersectionObserverEntry>();
private readonly localize = new LocalizeController(this);
private mutationObserver: MutationObserver;
@ -102,35 +99,10 @@ export default class SlCarousel extends ShoelaceElement {
super.connectedCallback();
this.setAttribute('role', 'region');
this.setAttribute('aria-label', this.localize.term('carousel'));
const intersectionObserver = new IntersectionObserver(
(entries: IntersectionObserverEntry[]) => {
entries.forEach(entry => {
// Store all the entries in a map to be processed when scrolling ends
this.intersectionObserverEntries.set(entry.target, entry);
const slide = entry.target;
slide.toggleAttribute('inert', !entry.isIntersecting);
slide.classList.toggle('--in-view', entry.isIntersecting);
slide.setAttribute('aria-hidden', entry.isIntersecting ? 'false' : 'true');
});
},
{
root: this,
threshold: 0.6
}
);
this.intersectionObserver = intersectionObserver;
// Store the initial state of each slide
intersectionObserver.takeRecords().forEach(entry => {
this.intersectionObserverEntries.set(entry.target, entry);
});
}
disconnectedCallback(): void {
super.disconnectedCallback();
this.intersectionObserver.disconnect();
this.mutationObserver.disconnect();
}
@ -291,26 +263,52 @@ export default class SlCarousel extends ShoelaceElement {
this.scrolling = true;
}
/** @internal Synchronizes the slides with the IntersectionObserver API. */
private synchronizeSlides() {
const io = new IntersectionObserver(
entries => {
io.disconnect();
for (const entry of entries) {
const slide = entry.target;
slide.toggleAttribute('inert', !entry.isIntersecting);
slide.classList.toggle('--in-view', entry.isIntersecting);
slide.setAttribute('aria-hidden', entry.isIntersecting ? 'false' : 'true');
}
const firstIntersecting = entries.find(entry => entry.isIntersecting);
if (firstIntersecting) {
if (this.loop && firstIntersecting.target.hasAttribute('data-clone')) {
const clonePosition = Number(firstIntersecting.target.getAttribute('data-clone'));
// Scrolls to the original slide without animating, so the user won't notice that the position has changed
this.goToSlide(clonePosition, 'instant');
} else {
const slides = this.getSlides();
// Update the current index based on the first visible slide
const slideIndex = slides.indexOf(firstIntersecting.target as SlCarouselItem);
// Set the index to the first "snappable" slide
this.activeSlide = Math.ceil(slideIndex / this.slidesPerMove) * this.slidesPerMove;
}
}
},
{
root: this.scrollContainer,
threshold: 0.6
}
);
this.getSlides({ excludeClones: false }).forEach(slide => {
io.observe(slide);
});
}
private handleScrollEnd() {
if (!this.scrolling || this.dragging) return;
const entries = [...this.intersectionObserverEntries.values()];
const firstIntersecting: IntersectionObserverEntry | undefined = entries.find(entry => entry.isIntersecting);
if (this.loop && firstIntersecting?.target.hasAttribute('data-clone')) {
const clonePosition = Number(firstIntersecting.target.getAttribute('data-clone'));
// Scrolls to the original slide without animating, so the user won't notice that the position has changed
this.goToSlide(clonePosition, 'instant');
} else if (firstIntersecting) {
const slides = this.getSlides();
// Update the current index based on the first visible slide
const slideIndex = slides.indexOf(firstIntersecting.target as SlCarouselItem);
// Set the index to the first "snappable" slide
this.activeSlide = Math.ceil(slideIndex / this.slidesPerMove) * this.slidesPerMove;
}
this.synchronizeSlides();
this.scrolling = false;
}
@ -337,14 +335,8 @@ export default class SlCarousel extends ShoelaceElement {
@watch('loop', { waitUntilFirstUpdate: true })
@watch('slidesPerPage', { waitUntilFirstUpdate: true })
initializeSlides() {
const intersectionObserver = this.intersectionObserver;
this.intersectionObserverEntries.clear();
// Removes all the cloned elements from the carousel
this.getSlides({ excludeClones: false }).forEach((slide, index) => {
intersectionObserver.unobserve(slide);
slide.classList.remove('--in-view');
slide.classList.remove('--is-active');
slide.setAttribute('aria-label', this.localize.term('slideNum', index + 1));
@ -361,9 +353,7 @@ export default class SlCarousel extends ShoelaceElement {
this.createClones();
}
this.getSlides({ excludeClones: false }).forEach(slide => {
intersectionObserver.observe(slide);
});
this.synchronizeSlides();
// Because the DOM may be changed, restore the scroll position to the active slide
this.goToSlide(this.activeSlide, 'auto');

Wyświetl plik

@ -1,21 +1,39 @@
import '../../../dist/shoelace.js';
import { aTimeout, expect, fixture, html, nextFrame, oneEvent, waitUntil } from '@open-wc/testing';
import { clickOnElement, dragElement, moveMouseOnElement } from '../../internal/test.js';
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';
import sinon from 'sinon';
import type { SinonStub } from 'sinon';
import type SlCarousel from './carousel.js';
describe('<sl-carousel>', () => {
const sandbox = sinon.createSandbox();
const ioCallbacks = new Map<IntersectionObserver, SinonStub>();
const intersectionObserverCallbacks = () => {
const callbacks = [...ioCallbacks.values()];
return waitUntil(() => callbacks.every(callback => callback.called));
};
const OriginalIntersectionObserver = globalThis.IntersectionObserver;
beforeEach(() => {
globalThis.IntersectionObserver = class IntersectionObserverMock extends OriginalIntersectionObserver {
constructor(callback: IntersectionObserverCallback, options?: IntersectionObserverInit) {
const stubCallback = sandbox.stub().callsFake(callback);
super(stubCallback, options);
ioCallbacks.set(this, stubCallback);
}
};
});
afterEach(async () => {
await resetMouse();
});
afterEach(() => {
sandbox.restore();
globalThis.IntersectionObserver = OriginalIntersectionObserver;
ioCallbacks.clear();
});
it('should render a carousel with default configuration', async () => {
@ -311,6 +329,7 @@ describe('<sl-carousel>', () => {
await clickOnElement(nextButton);
await oneEvent(el.scrollContainer, 'scrollend');
await intersectionObserverCallbacks();
await el.updateComplete;
// Assert
@ -337,13 +356,19 @@ describe('<sl-carousel>', () => {
// Act
await clickOnElement(nextButton);
await aTimeout(50);
await clickOnElement(nextButton);
await aTimeout(50);
await clickOnElement(nextButton);
await aTimeout(50);
await clickOnElement(nextButton);
await aTimeout(50);
await clickOnElement(nextButton);
await aTimeout(50);
await clickOnElement(nextButton);
await oneEvent(el.scrollContainer, 'scrollend');
await intersectionObserverCallbacks();
await el.updateComplete;
// Assert
@ -502,6 +527,7 @@ describe('<sl-carousel>', () => {
el.goToSlide(2, 'auto');
await oneEvent(el.scrollContainer, 'scrollend');
await intersectionObserverCallbacks();
await el.updateComplete;
// Act
@ -537,6 +563,9 @@ describe('<sl-carousel>', () => {
// wait scroll to actual item
await oneEvent(el.scrollContainer, 'scrollend');
await intersectionObserverCallbacks();
await el.updateComplete;
// Assert
expect(nextButton).to.have.attribute('aria-disabled', 'false');
expect(el.activeSlide).to.be.equal(0);
@ -620,6 +649,8 @@ describe('<sl-carousel>', () => {
// wait scroll to actual item
await oneEvent(el.scrollContainer, 'scrollend');
await intersectionObserverCallbacks();
// Assert
expect(previousButton).to.have.attribute('aria-disabled', 'false');
expect(el.activeSlide).to.be.equal(2);
@ -673,6 +704,7 @@ describe('<sl-carousel>', () => {
el.goToSlide(1);
await oneEvent(el.scrollContainer, 'scrollend');
await intersectionObserverCallbacks();
await nextFrame();
sandbox.spy(el, 'goToSlide');
@ -680,6 +712,7 @@ describe('<sl-carousel>', () => {
// Act
el.previous();
await oneEvent(el.scrollContainer, 'scrollend');
await intersectionObserverCallbacks();
const containerRect = el.scrollContainer.getBoundingClientRect();
const itemRect = expectedCarouselItem.getBoundingClientRect();
@ -706,6 +739,7 @@ describe('<sl-carousel>', () => {
// Act
el.goToSlide(2);
await oneEvent(el.scrollContainer, 'scrollend');
await intersectionObserverCallbacks();
await el.updateComplete;
// Assert

Wyświetl plik

@ -3,6 +3,7 @@ import { classMap } from 'lit/directives/class-map.js';
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry.js';
import { getTabbableBoundary } from '../../internal/tabbable.js';
import { html } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js';
import { LocalizeController } from '../../utilities/localize.js';
import { property, query } from 'lit/decorators.js';
import { waitForEvent } from '../../internal/event.js';
@ -102,6 +103,11 @@ export default class SlDropdown extends ShoelaceElement {
*/
@property({ type: Boolean }) hoist = false;
/**
* Syncs the popup width or height to that of the trigger element.
*/
@property({ reflect: true }) sync: 'width' | 'height' | 'both' | undefined = undefined;
connectedCallback() {
super.connectedCallback();
@ -409,6 +415,7 @@ export default class SlDropdown extends ShoelaceElement {
shift
auto-size="vertical"
auto-size-padding="10"
sync=${ifDefined(this.sync ? this.sync : undefined)}
class=${classMap({
dropdown: true,
'dropdown--open': this.open

Wyświetl plik

@ -42,9 +42,21 @@ export default class SlIcon extends ShoelaceElement {
let fileData: Response;
if (library?.spriteSheet) {
return html`<svg part="svg">
this.svg = html`<svg part="svg">
<use part="use" href="${url}"></use>
</svg>`;
// Using a templateResult requires the SVG to be written to the DOM first before we can grab the SVGElement
// to be passed to the library's mutator function.
await this.updateComplete;
const svg = this.shadowRoot!.querySelector("[part='svg']")!;
if (typeof library.mutator === 'function') {
library.mutator(svg as SVGElement);
}
return this.svg;
}
try {

Wyświetl plik

@ -204,6 +204,10 @@ describe('<sl-icon>', () => {
const rect = use?.getBoundingClientRect();
expect(rect?.width).to.equal(0);
expect(rect?.width).to.equal(0);
// Make sure the mutator is applied.
// https://github.com/shoelace-style/shoelace/issues/1925
expect(svg?.getAttribute('fill')).to.equal('currentColor');
});
// TODO: <use> svg icons don't emit a "load" or "error" event...if we can figure out how to get the event to emit errors.

Wyświetl plik

@ -251,13 +251,16 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
}
private handleClearClick(event: MouseEvent) {
this.value = '';
this.emit('sl-clear');
this.emit('sl-input');
this.emit('sl-change');
this.input.focus();
event.preventDefault();
event.stopPropagation();
if (this.value !== '') {
this.value = '';
this.emit('sl-clear');
this.emit('sl-input');
this.emit('sl-change');
}
this.input.focus();
}
private handleFocus() {
@ -493,14 +496,11 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont
@blur=${this.handleBlur}
/>
${hasClearIcon
${isClearIconVisible
? html`
<button
part="clear-button"
class=${classMap({
input__clear: true,
'input__clear--visible': isClearIconVisible
})}
class="input__clear"
type="button"
aria-label=${this.localize.term('clearEntry')}
@click=${this.handleClearClick}

Wyświetl plik

@ -247,10 +247,6 @@ export default css`
* Clearable + Password Toggle
*/
.input__clear:not(.input__clear--visible) {
visibility: hidden;
}
.input__clear,
.input__password-toggle {
display: inline-flex;
@ -275,10 +271,6 @@ export default css`
outline: none;
}
.input--empty .input__clear {
visibility: hidden;
}
/* Don't show the browser's password toggle in Edge */
::-ms-reveal {
display: none;

Wyświetl plik

@ -46,6 +46,8 @@ import type { CSSResultGroup, PropertyValueMap } from 'lit';
* @csspart item--selected - Applied when the tree item is selected.
* @csspart indentation - The tree item's indentation container.
* @csspart expand-button - The container that wraps the tree item's expand button and spinner.
* @csspart spinner - The spinner that shows when a lazy tree item is in the loading state.
* @csspart spinner__base - The spinner's base part.
* @csspart label - The tree item's label.
* @csspart children - The container that wraps the tree item's nested children.
* @csspart checkbox - The checkbox that shows when using multiselect.
@ -257,7 +259,10 @@ export default class SlTreeItem extends ShoelaceElement {
})}
aria-hidden="true"
>
${when(this.loading, () => html` <sl-spinner></sl-spinner> `)}
${when(
this.loading,
() => html` <sl-spinner part="spinner" exportparts="base:spinner__base"></sl-spinner> `
)}
<slot class="tree-item__expand-icon-slot" name="expand-icon">
<sl-icon library="system" name=${isRtl ? 'chevron-left' : 'chevron-right'}></sl-icon>
</slot>

Wyświetl plik

@ -144,12 +144,16 @@ export default class SlTree extends ShoelaceElement {
.forEach((status: 'expand' | 'collapse') => {
const existingIcon = item.querySelector(`[slot="${status}-icon"]`);
const expandButtonIcon = this.getExpandButtonIcon(status);
if (!expandButtonIcon) return;
if (existingIcon === null) {
// No separator exists, add one
item.append(this.getExpandButtonIcon(status)!);
item.append(expandButtonIcon);
} else if (existingIcon.hasAttribute('data-default')) {
// A default separator exists, replace it
existingIcon.replaceWith(this.getExpandButtonIcon(status)!);
existingIcon.replaceWith(expandButtonIcon);
} else {
// The user provided a custom icon, leave it alone
}

Wyświetl plik

@ -752,4 +752,29 @@ describe('<sl-tree>', () => {
});
});
});
// https://github.com/shoelace-style/shoelace/issues/1916
it("Should not render 'null' if it can't find a custom icon", async () => {
const tree = await fixture<SlTree>(html`
<sl-tree>
<sl-tree-item>
Item 1
<sl-icon name="1-circle" slot="expand-icon"></sl-icon>
<sl-tree-item> Item A </sl-tree-item>
</sl-tree-item>
<sl-tree-item>
Item 2
<sl-tree-item>Item A</sl-tree-item>
<sl-tree-item>Item B</sl-tree-item>
</sl-tree-item>
<sl-tree-item>
Item 3
<sl-tree-item>Item A</sl-tree-item>
<sl-tree-item>Item B</sl-tree-item>
</sl-tree-item>
</sl-tree>
`);
expect(tree.textContent).to.not.includes('null');
});
});