kopia lustrzana https://github.com/shoelace-style/shoelace
Merge branch 'next' into current
commit
5ef6d6bc2f
|
@ -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="/">
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
```
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -57,6 +57,7 @@ export default css`
|
|||
|
||||
.rating__symbol {
|
||||
transition: var(--sl-transition-fast) scale;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.rating__symbol--hover {
|
||||
|
|
|
@ -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``;
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
Ładowanie…
Reference in New Issue