Merge branch 'next' into current

current
Cory LaViska 2024-03-25 14:12:33 -04:00
commit 5ef6d6bc2f
36 zmienionych plików z 448 dodań i 152 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

@ -1839,3 +1839,15 @@ const App = () => {
);
};
```
Sometimes the `getBoundingClientRects` might be derived from a real element. In this case provide the anchor element as context to ensure clipping and position updates for the popup work well.
```ts
const virtualElement = {
getBoundingClientRect() {
// ...
return { width, height, x, y, top, left, right, bottom };
},
contextElement: anchorElement
};
```

Wyświetl plik

@ -12,6 +12,26 @@ Components with the <sl-badge variant="warning" pill>Experimental</sl-badge> 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).
## 2.15.0
- Added the Slovenian translation [#1893]
- Added support for `contextElement` to `VirtualElements` in `<sl-popup>` [#1874]
- 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]
- Fixed a bug in `<sl-tab-group>` that caused the scroll controls to toggle indefinitely when zoomed in Safari [#1839]
- Fixed a bug in the submenu controller that allowed two submenus to be open at the same time [#1880]
- 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]
- Fixed a bug in `<sl-button-group>` that caused styles to stop working when using `className` on buttons in React [#1926]
## 2.14.0
- Added the Arabic translation [#1852]
@ -30,6 +50,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]

4
package-lock.json wygenerowano
Wyświetl plik

@ -1,12 +1,12 @@
{
"name": "@shoelace-style/shoelace",
"version": "2.14.0",
"version": "2.15.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@shoelace-style/shoelace",
"version": "2.14.0",
"version": "2.15.0",
"license": "MIT",
"dependencies": {
"@ctrl/tinycolor": "^4.0.2",

Wyświetl plik

@ -1,7 +1,7 @@
{
"name": "@shoelace-style/shoelace",
"description": "A forward-thinking library of web components.",
"version": "2.14.0",
"version": "2.15.0",
"homepage": "https://github.com/shoelace-style/shoelace",
"author": "Cory LaViska",
"license": "MIT",

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', false);
}
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

@ -27,8 +27,8 @@ describe('<sl-button-group>', () => {
});
});
describe('slotted button classes', () => {
it('slotted buttons have the right classes applied based on their order', async () => {
describe('slotted button data attributes', () => {
it('slotted buttons have the right data attributes applied based on their order', async () => {
const group = await fixture<SlButtonGroup>(html`
<sl-button-group>
<sl-button>Button 1 Label</sl-button>
@ -38,19 +38,19 @@ describe('<sl-button-group>', () => {
`);
const allButtons = group.querySelectorAll('sl-button');
const hasGroupClass = Array.from(allButtons).every(button =>
button.classList.contains('sl-button-group__button')
const hasGroupAttrib = Array.from(allButtons).every(button =>
button.hasAttribute('data-sl-button-group__button')
);
expect(hasGroupClass).to.be.true;
expect(hasGroupAttrib).to.be.true;
expect(allButtons[0]).to.have.class('sl-button-group__button--first');
expect(allButtons[1]).to.have.class('sl-button-group__button--inner');
expect(allButtons[2]).to.have.class('sl-button-group__button--last');
expect(allButtons[0]).to.have.attribute('data-sl-button-group__button--first');
expect(allButtons[1]).to.have.attribute('data-sl-button-group__button--inner');
expect(allButtons[2]).to.have.attribute('data-sl-button-group__button--last');
});
});
describe('focus and blur events', () => {
it('toggles focus class to slotted buttons on focus/blur', async () => {
it('toggles focus data attribute to slotted buttons on focus/blur', async () => {
const group = await fixture<SlButtonGroup>(html`
<sl-button-group>
<sl-button>Button 1 Label</sl-button>
@ -63,16 +63,16 @@ describe('<sl-button-group>', () => {
allButtons[0].dispatchEvent(new FocusEvent('focusin', { bubbles: true }));
await elementUpdated(allButtons[0]);
expect(allButtons[0].classList.contains('sl-button-group__button--focus')).to.be.true;
expect(allButtons[0]).to.have.attribute('data-sl-button-group__button--focus');
allButtons[0].dispatchEvent(new FocusEvent('focusout', { bubbles: true }));
await elementUpdated(allButtons[0]);
expect(allButtons[0].classList.contains('sl-button-group__button--focus')).not.to.be.true;
expect(allButtons[0]).to.not.have.attribute('data-sl-button-group__button--focus');
});
});
describe('mouseover and mouseout events', () => {
it('toggles hover class to slotted buttons on mouseover/mouseout', async () => {
it('toggles hover data attribute to slotted buttons on mouseover/mouseout', async () => {
const group = await fixture<SlButtonGroup>(html`
<sl-button-group>
<sl-button>Button 1 Label</sl-button>
@ -85,11 +85,12 @@ describe('<sl-button-group>', () => {
allButtons[0].dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
await elementUpdated(allButtons[0]);
expect(allButtons[0].classList.contains('sl-button-group__button--hover')).to.be.true;
expect(allButtons[0]).to.have.attribute('data-sl-button-group__button--hover');
allButtons[0].dispatchEvent(new MouseEvent('mouseout', { bubbles: true }));
await elementUpdated(allButtons[0]);
expect(allButtons[0].classList.contains('sl-button-group__button--hover')).not.to.be.true;
console.log(allButtons[0]);
expect(allButtons[0]).to.not.have.attribute('data-sl-button-group__button--hover');
});
});
});

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

@ -8,6 +8,7 @@ import { live } from 'lit/directives/live.js';
import { property, query, state } from 'lit/decorators.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import formControlStyles from '../../styles/form-control.styles.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import SlIcon from '../icon/icon.component.js';
import styles from './checkbox.styles.js';
@ -41,7 +42,7 @@ import type { ShoelaceFormControl } from '../../internal/shoelace-element.js';
* @csspart form-control-help-text - The help text's wrapper.
*/
export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormControl {
static styles: CSSResultGroup = [componentStyles, styles];
static styles: CSSResultGroup = [componentStyles, formControlStyles, styles];
static dependencies = { 'sl-icon': SlIcon };
private readonly formControlController = new FormControlController(this, {

Wyświetl plik

@ -115,6 +115,7 @@ export default css`
:host([required]) .checkbox__label::after {
content: var(--sl-input-required-content);
color: var(--sl-input-required-content-color);
margin-inline-start: var(--sl-input-required-content-offset);
}
`;

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

@ -229,6 +229,7 @@ export class SubmenuController implements ReactiveController {
// newly opened menu.
private enableSubmenu(delay = true) {
if (delay) {
window.clearTimeout(this.enableSubmenuTimer);
this.enableSubmenuTimer = window.setTimeout(() => {
this.setSubmenuState(true);
}, this.submenuOpenDelay);
@ -238,7 +239,7 @@ export class SubmenuController implements ReactiveController {
}
private disableSubmenu() {
clearTimeout(this.enableSubmenuTimer);
window.clearTimeout(this.enableSubmenuTimer);
this.setSubmenuState(false);
}

Wyświetl plik

@ -10,10 +10,16 @@ import type { CSSResultGroup } from 'lit';
export interface VirtualElement {
getBoundingClientRect: () => DOMRect;
contextElement?: Element;
}
function isVirtualElement(e: unknown): e is VirtualElement {
return e !== null && typeof e === 'object' && 'getBoundingClientRect' in e;
return (
e !== null &&
typeof e === 'object' &&
'getBoundingClientRect' in e &&
('contextElement' in e ? e instanceof Element : true)
);
}
/**

Wyświetl plik

@ -21,7 +21,7 @@ describe('<sl-radio-button>', () => {
expect(radio2.checked).to.be.false;
});
it('should receive positional classes from <sl-button-group>', async () => {
it('should receive positional data attributes from <sl-button-group>', async () => {
const radioGroup = await fixture<SlRadioGroup>(html`
<sl-radio-group value="1">
<sl-radio-button id="radio-1" value="1"></sl-radio-button>
@ -35,11 +35,11 @@ describe('<sl-radio-button>', () => {
await Promise.all([radioGroup.updateComplete, radio1.updateComplete, radio2.updateComplete, radio3.updateComplete]);
expect(radio1.classList.contains('sl-button-group__button')).to.be.true;
expect(radio1.classList.contains('sl-button-group__button--first')).to.be.true;
expect(radio2.classList.contains('sl-button-group__button')).to.be.true;
expect(radio2.classList.contains('sl-button-group__button--inner')).to.be.true;
expect(radio3.classList.contains('sl-button-group__button')).to.be.true;
expect(radio3.classList.contains('sl-button-group__button--last')).to.be.true;
expect(radio1).to.have.attribute('data-sl-button-group__button');
expect(radio1).to.have.attribute('data-sl-button-group__button--first');
expect(radio2).to.have.attribute('data-sl-button-group__button');
expect(radio2).to.have.attribute('data-sl-button-group__button--inner');
expect(radio3).to.have.attribute('data-sl-button-group__button');
expect(radio3).to.have.attribute('data-sl-button-group__button--last');
});
});

Wyświetl plik

@ -264,7 +264,6 @@ export default class SlRating extends ShoelaceElement {
'rating__symbol--hover': this.isHovering && Math.ceil(displayValue) === index + 1
})}
role="presentation"
@mouseenter=${this.handleMouseEnter}
>
<div
style=${styleMap({
@ -297,7 +296,6 @@ export default class SlRating extends ShoelaceElement {
'rating__symbol--active': displayValue >= index + 1
})}
role="presentation"
@mouseenter=${this.handleMouseEnter}
>
${unsafeHTML(this.getSymbol(index + 1))}
</span>

Wyświetl plik

@ -57,6 +57,7 @@ export default css`
.rating__symbol {
transition: var(--sl-transition-fast) scale;
pointer-events: none;
}
.rating__symbol--hover {

Wyświetl plik

@ -224,7 +224,15 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
//
// https://github.com/shoelace-style/shoelace/issues/1763
//
const root = this.getRootNode();
document.addEventListener('focusin', this.handleDocumentFocusIn);
document.addEventListener('keydown', this.handleDocumentKeyDown);
document.addEventListener('mousedown', this.handleDocumentMouseDown);
// If the component is rendered in a shadow root, we need to attach the focusin listener there too
if (this.getRootNode() !== document) {
this.getRootNode().addEventListener('focusin', this.handleDocumentFocusIn);
}
if ('CloseWatcher' in window) {
this.closeWatcher?.destroy();
this.closeWatcher = new CloseWatcher();
@ -235,16 +243,17 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
}
};
}
root.addEventListener('focusin', this.handleDocumentFocusIn);
root.addEventListener('keydown', this.handleDocumentKeyDown);
root.addEventListener('mousedown', this.handleDocumentMouseDown);
}
private removeOpenListeners() {
const root = this.getRootNode();
root.removeEventListener('focusin', this.handleDocumentFocusIn);
root.removeEventListener('keydown', this.handleDocumentKeyDown);
root.removeEventListener('mousedown', this.handleDocumentMouseDown);
document.removeEventListener('focusin', this.handleDocumentFocusIn);
document.removeEventListener('keydown', this.handleDocumentKeyDown);
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
if (this.getRootNode() !== document) {
this.getRootNode().removeEventListener('focusin', this.handleDocumentFocusIn);
}
this.closeWatcher?.destroy();
}
@ -608,7 +617,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
</div>`;
} else if (index === this.maxOptionsVisible) {
// Hit tag limit
return html`<sl-tag>+${this.selectedOptions.length - index}</sl-tag>`;
return html`<sl-tag size=${this.size}>+${this.selectedOptions.length - index}</sl-tag>`;
}
return html``;
});

Wyświetl plik

@ -462,7 +462,7 @@ describe('<sl-select>', () => {
await select.updateComplete;
expect(select.value).to.equal('option-3');
setTimeout(() => clickOnElement(resetButton));
setTimeout(() => resetButton.click());
await oneEvent(form, 'reset');
await select.updateComplete;
expect(select.value).to.equal('option-1');

Wyświetl plik

@ -155,6 +155,7 @@ export default css`
:host([required]) .switch__label::after {
content: var(--sl-input-required-content);
color: var(--sl-input-required-content-color);
margin-inline-start: var(--sl-input-required-content-offset);
}

Wyświetl plik

@ -338,8 +338,13 @@ export default class SlTabGroup extends ShoelaceElement {
if (this.noScrollControls) {
this.hasScrollControls = false;
} else {
// In most cases, we can compare scrollWidth to clientWidth to determine if scroll controls should show. However,
// Safari appears to calculate this incorrectly when zoomed at 110%, causing the controls to toggle indefinitely.
// Adding a single pixel to the comparison seems to resolve it.
//
// See https://github.com/shoelace-style/shoelace/issues/1839
this.hasScrollControls =
['top', 'bottom'].includes(this.placement) && this.nav.scrollWidth > this.nav.clientWidth;
['top', 'bottom'].includes(this.placement) && this.nav.scrollWidth > this.nav.clientWidth + 1;
}
}

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

@ -13,7 +13,6 @@ export default css`
--indent-size: var(--sl-spacing-large);
display: block;
isolation: isolate;
/*
* Tree item indentation uses the "em" unit to increment its width on each level, so setting the font size to zero

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');
});
});

Wyświetl plik

@ -72,9 +72,8 @@ export class FormControlController implements ReactiveController {
const formId = input.form;
if (formId) {
const root = input.getRootNode() as Document | ShadowRoot;
const form = root.getElementById(formId);
const root = input.getRootNode() as Document | ShadowRoot | HTMLElement;
const form = root.querySelector(`#${formId}`);
if (form) {
return form as HTMLFormElement;

Wyświetl plik

@ -8,6 +8,19 @@ function getScrollbarWidth() {
return Math.abs(window.innerWidth - documentWidth);
}
/**
* Used in conjunction with `scrollbarWidth` to set proper body padding in case the user has padding already on the `<body>` element.
*/
function getExistingBodyPadding() {
const padding = Number(getComputedStyle(document.body).paddingRight.replace(/px/, ''));
if (isNaN(padding) || !padding) {
return 0;
}
return padding;
}
/**
* Prevents body scrolling. Keeps track of which elements requested a lock so multiple levels of locking are possible
* without premature unlocking.
@ -17,10 +30,11 @@ export function lockBodyScrolling(lockingEl: HTMLElement) {
// When the first lock is created, set the scroll lock size to match the scrollbar's width to prevent content from
// shifting. We only do this on the first lock because the scrollbar width will measure zero after overflow is hidden.
if (!document.body.classList.contains('sl-scroll-lock')) {
const scrollbarWidth = getScrollbarWidth(); // must be measured before the `sl-scroll-lock` class is applied
document.body.classList.add('sl-scroll-lock');
document.body.style.setProperty('--sl-scroll-lock-size', `${scrollbarWidth}px`);
if (!document.documentElement.classList.contains('sl-scroll-lock')) {
/** Scrollbar width + body padding calculation can go away once Safari has scrollbar-gutter support. */
const scrollbarWidth = getScrollbarWidth() + getExistingBodyPadding(); // must be measured before the `sl-scroll-lock` class is applied
document.documentElement.classList.add('sl-scroll-lock');
document.documentElement.style.setProperty('--sl-scroll-lock-size', `${scrollbarWidth}px`);
}
}
@ -31,8 +45,8 @@ export function unlockBodyScrolling(lockingEl: HTMLElement) {
locks.delete(lockingEl);
if (locks.size === 0) {
document.body.classList.remove('sl-scroll-lock');
document.body.style.removeProperty('--sl-scroll-lock-size');
document.documentElement.classList.remove('sl-scroll-lock');
document.documentElement.style.removeProperty('--sl-scroll-lock-size');
}
}

Wyświetl plik

@ -145,7 +145,7 @@ it('Should allow tabbing to slotted elements', async () => {
expect(activeElementsArray()).to.include(focusSix);
});
it('Should account for when focus is changed from outside sources (like clicking)', async () => {
it.skip('Should account for when focus is changed from outside sources (like clicking)', async () => {
const dialog = await fixture(html`
<sl-dialog open="" label="Dialog" class="dialog-overview">
Lorem ipsum dolor sit amet, consectetur adipiscing elit.

Wyświetl plik

@ -11,8 +11,9 @@
}
}
/** This can go away once Safari has scrollbar-gutter support. */
@supports not (scrollbar-gutter: stable) {
.sl-scroll-lock {
.sl-scroll-lock body {
padding-right: var(--sl-scroll-lock-size) !important;
overflow: hidden !important;
}

Wyświetl plik

@ -0,0 +1,41 @@
import { registerTranslation } from '@shoelace-style/localize';
import type { Translation } from '../utilities/localize.js';
const translation: Translation = {
$code: 'sl',
$name: 'Slovenski',
$dir: 'ltr',
carousel: 'Vrtiljak',
clearEntry: 'Počisti vnos',
close: 'Zapri',
copied: 'Kopirano',
copy: 'Kopiraj',
currentValue: 'Trenutna vrednost',
error: 'Napaka',
goToSlide: (slide, count) => `Pojdi na diapozitiv ${slide} od ${count}`,
hidePassword: 'Skrij geslo',
loading: 'Nalaganje',
nextSlide: 'Naslednji diapozitiv',
numOptionsSelected: num => {
if (num === 0) return 'Nobena možnost ni izbrana';
if (num === 1) return '1 možnost izbrana';
if (num === 2) return '2 možnosti izbrani';
if (num === 3 || num === 4) return `${num} možnosti izbrane`;
return `${num} možnosti izbranih`;
},
previousSlide: 'Prejšnji diapozitiv',
progress: 'Napredek',
remove: 'Odstrani',
resize: 'Spremeni velikost',
scrollToEnd: 'Pomakni se na konec',
scrollToStart: 'Pomakni se na začetek',
selectAColorFromTheScreen: 'Izberite barvo z zaslona',
showPassword: 'Prikaži geslo',
slideNum: slide => `Diapozitiv ${slide}`,
toggleColorFormat: 'Preklopi format barve'
};
registerTranslation(translation);
export default translation;