Skip to content

Commit

Permalink
Merge branch 'shoelace-style:next' into next
Browse files Browse the repository at this point in the history
  • Loading branch information
carlos-verdes authored Oct 30, 2024
2 parents b1c6834 + 73c469f commit b3d916f
Show file tree
Hide file tree
Showing 14 changed files with 113 additions and 61 deletions.
15 changes: 13 additions & 2 deletions docs/pages/resources/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,26 @@ New versions of Shoelace are released as-needed and generally occur when a criti

## Next

- Updated all checks for directionality to use `this.localize.dir()` instead of `el.matches(:dir(rtl))` so older browsers don't error out [#2188]
- Added Finnish translations [#2211]
- Added support for <kbd>Enter</kbd> to `<sl-split-panel>` to align with ARIA APG's [window splitter pattern](https://www.w3.org/WAI/ARIA/apg/patterns/windowsplitter/) [#2234]
- Fixed a bug in `<sl-carousel>` that caused the navigation icons to be reversed
- Fixed a bug in `<sl-select>` that prevented label changes in `<sl-option>` from updating the controller [#1971]
- Fixed a bug in `<sl-textarea>` that caused a console warning in Firefox when typing [#2107]
- Improved performance of `<sl-range>` by skipping positioning logic when tooltip isn't shown [#2064]

## 2.18.0

- Added Finnish translation [#2211]
- Added the `.focus` function to `<sl-radio-group>` [#2192]
- Fixed a bug in `<sl-tab-group>` when removed from the DOM too quickly. [#2218]
- Fixed a bug with `<sl-select>` not respecting its initial value. [#2204]
- Fixed a bug with certain bundlers when using dynamic imports [#2210]
- Fixed a bug in `<sl-textarea>` causing scroll jumping when using `resize="auto"` [#2182]
- Fixed a bug in `<sl-relative-time>` where the title attribute would show with redundant info [#2184]
- Fixed a bug in `<sl-select>` that caused multi-selects without placeholders to have the wrong padding [#2194]
- Fixed a bug in `<sl-tooltip>` that caused a memory leak in disconnected elements [#2226]
- Fixed a bug in `<sl-select>` that caused an exception in an edge case using Edge + autofill [#2221]
- Improved the behavior of navigation dots in `<sl-carousel>` [#2220]
- Updated all checks for directionality to use `this.localize.dir()` instead of `el.matches(:dir(rtl))` so older browsers don't error out [#2188]

## 2.17.1

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@shoelace-style/shoelace",
"description": "A forward-thinking library of web components.",
"version": "2.17.1",
"version": "2.18.0",
"homepage": "https://github.com/shoelace-style/shoelace",
"author": "Cory LaViska",
"license": "MIT",
Expand Down
50 changes: 32 additions & 18 deletions src/components/carousel/carousel.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export default class SlCarousel extends ShoelaceElement {
private autoplayController = new AutoplayController(this, () => this.next());
private readonly localize = new LocalizeController(this);
private mutationObserver: MutationObserver;
private pendingSlideChange = false;

connectedCallback(): void {
super.connectedCallback();
Expand Down Expand Up @@ -261,6 +262,9 @@ export default class SlCarousel extends ShoelaceElement {
@eventOptions({ passive: true })
private handleScroll() {
this.scrolling = true;
if (!this.pendingSlideChange) {
this.synchronizeSlides();
}
}

/** @internal Synchronizes the slides with the IntersectionObserver API. */
Expand All @@ -277,20 +281,28 @@ export default class SlCarousel extends ShoelaceElement {
}

const firstIntersecting = entries.find(entry => entry.isIntersecting);
if (!firstIntersecting) {
return;
}

const slidesWithClones = this.getSlides({ excludeClones: false });
const slidesCount = this.getSlides().length;

// Update the current index based on the first visible slide
const slideIndex = slidesWithClones.indexOf(firstIntersecting.target as SlCarouselItem);
// Normalize the index to ignore clones
const normalizedIndex = this.loop ? slideIndex - this.slidesPerPage : slideIndex;

if (firstIntersecting) {
// Set the index to the closest "snappable" slide
this.activeSlide =
(Math.ceil(normalizedIndex / this.slidesPerMove) * this.slidesPerMove + slidesCount) % slidesCount;

if (!this.scrolling) {
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;
}
}
},
Expand All @@ -307,10 +319,9 @@ export default class SlCarousel extends ShoelaceElement {

private handleScrollEnd() {
if (!this.scrolling || this.dragging) return;

this.synchronizeSlides();

this.scrolling = false;
this.pendingSlideChange = false;
this.synchronizeSlides();
}

private isCarouselItem(node: Node): node is SlCarouselItem {
Expand Down Expand Up @@ -380,7 +391,7 @@ export default class SlCarousel extends ShoelaceElement {
}

@watch('activeSlide')
handelSlideChange() {
handleSlideChange() {
const slides = this.getSlides();
slides.forEach((slide, i) => {
slide.classList.toggle('--is-active', i === this.activeSlide);
Expand Down Expand Up @@ -485,11 +496,14 @@ export default class SlCarousel extends ShoelaceElement {
const nextLeft = nextSlideRect.left - scrollContainerRect.left;
const nextTop = nextSlideRect.top - scrollContainerRect.top;

scrollContainer.scrollTo({
left: nextLeft + scrollContainer.scrollLeft,
top: nextTop + scrollContainer.scrollTop,
behavior
});
if (nextLeft || nextTop) {
this.pendingSlideChange = true;
scrollContainer.scrollTo({
left: nextLeft + scrollContainer.scrollLeft,
top: nextTop + scrollContainer.scrollTop,
behavior
});
}
}

render() {
Expand All @@ -498,7 +512,7 @@ export default class SlCarousel extends ShoelaceElement {
const currentPage = this.getCurrentPage();
const prevEnabled = this.canScrollPrev();
const nextEnabled = this.canScrollNext();
const isLtr = this.localize.dir() === 'rtl';
const isLtr = this.localize.dir() === 'ltr';

return html`
<div part="base" class="carousel">
Expand Down
2 changes: 1 addition & 1 deletion src/components/color-picker/color-picker.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1053,7 +1053,7 @@ export default class SlColorPicker extends ShoelaceElement implements ShoelaceFo
<sl-dropdown
class="color-dropdown"
aria-disabled=${this.disabled ? 'true' : 'false'}
.containing-element=${this}
.containingElement=${this}
?disabled=${this.disabled}
?hoist=${this.hoist}
@sl-after-hide=${this.handleAfterHide}
Expand Down
21 changes: 7 additions & 14 deletions src/components/option/option.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export default class SlOption extends ShoelaceElement {
static styles: CSSResultGroup = [componentStyles, styles];
static dependencies = { 'sl-icon': SlIcon };

private cachedTextLabel: string;
// @ts-expect-error - Controller is currently unused
private readonly localize = new LocalizeController(this);

Expand All @@ -58,19 +57,13 @@ export default class SlOption extends ShoelaceElement {
}

private handleDefaultSlotChange() {
const textLabel = this.getTextLabel();

// Ignore the first time the label is set
if (typeof this.cachedTextLabel === 'undefined') {
this.cachedTextLabel = textLabel;
return;
}

// When the label changes, emit a slotchange event so parent controls see it
if (textLabel !== this.cachedTextLabel) {
this.cachedTextLabel = textLabel;
this.emit('slotchange', { bubbles: true, composed: false, cancelable: false });
}
// When the label changes, tell the controller to update
customElements.whenDefined('sl-select').then(() => {
const controller = this.closest('sl-select');
if (controller) {
controller.handleDefaultSlotChange();
}
});
}

private handleMouseEnter() {
Expand Down
14 changes: 1 addition & 13 deletions src/components/option/option.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import '../../../dist/shoelace.js';
import { aTimeout, expect, fixture, html, waitUntil } from '@open-wc/testing';
import sinon from 'sinon';
import { aTimeout, expect, fixture, html } from '@open-wc/testing';
import type SlOption from './option.js';

describe('<sl-option>', () => {
Expand Down Expand Up @@ -32,17 +31,6 @@ describe('<sl-option>', () => {
expect(el.getAttribute('aria-disabled')).to.equal('true');
});

it('emits the slotchange event when the label changes', async () => {
const el = await fixture<SlOption>(html` <sl-option>Text</sl-option> `);
const slotChangeHandler = sinon.spy();

el.addEventListener('slotchange', slotChangeHandler);
el.textContent = 'New Text';
await waitUntil(() => slotChangeHandler.calledOnce);

expect(slotChangeHandler).to.have.been.calledOnce;
});

it('should convert non-string values to string', async () => {
const el = await fixture<SlOption>(html` <sl-option>Text</sl-option> `);

Expand Down
2 changes: 1 addition & 1 deletion src/components/range/range.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ export default class SlRange extends ShoelaceElement implements ShoelaceFormCont

this.syncProgress(percent);

if (this.tooltip !== 'none') {
if (this.tooltip !== 'none' && this.hasTooltip) {
// Ensure updates are drawn before we sync the tooltip
this.updateComplete.then(() => this.syncTooltip(percent));
}
Expand Down
5 changes: 3 additions & 2 deletions src/components/select/select.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
}

// All other "printable" keys trigger type to select
if (event.key.length === 1 || event.key === 'Backspace') {
if ((event.key && event.key.length === 1) || event.key === 'Backspace') {
const allOptions = this.getAllOptions();

// Don't block important key combos like CMD+R
Expand Down Expand Up @@ -501,7 +501,8 @@ export default class SlSelect extends ShoelaceElement implements ShoelaceFormCon
}
}

private handleDefaultSlotChange() {
/* @internal - used by options to update labels */
public handleDefaultSlotChange() {
if (!customElements.get('wa-option')) {
customElements.whenDefined('wa-option').then(() => this.handleDefaultSlotChange());
}
Expand Down
27 changes: 26 additions & 1 deletion src/components/select/select.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,29 @@ describe('<sl-select>', () => {

expect(handler).to.be.calledTwice;
});

// this can happen in on ms-edge autofilling an associated input element in the same form
// https://github.com/shoelace-style/shoelace/issues/2117
it('should not throw on incomplete events', async () => {
const el = await fixture<SlSelect>(html`
<sl-select required>
<sl-option value="option-1">Option 1</sl-option>
</sl-select>
`);

const event = new KeyboardEvent('keydown');
Object.defineProperty(event, 'target', { writable: false, value: el });
Object.defineProperty(event, 'key', { writable: false, value: undefined });

/**
* If Edge does autofill, it creates a broken KeyboardEvent
* which is missing the key value.
* Using the normal dispatch mechanism does not allow to do this
* Thus passing the event directly to the private method for testing
*
* @ts-expect-error */
el.handleDocumentKeyDown(event);
});
});

it('should open the listbox when any letter key is pressed with sl-select is on focus', async () => {
Expand Down Expand Up @@ -483,7 +506,9 @@ describe('<sl-select>', () => {
expect(displayInput.value).to.equal('Option 1');

option.textContent = 'updated';
await oneEvent(option, 'slotchange');

await aTimeout(0);
await option.updateComplete;
await el.updateComplete;

expect(displayInput.value).to.equal('updated');
Expand Down
24 changes: 23 additions & 1 deletion src/components/split-panel/split-panel.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ export default class SlSplitPanel extends ShoelaceElement {
static styles: CSSResultGroup = [componentStyles, styles];

private cachedPositionInPixels: number;
private isCollapsed = false;
private readonly localize = new LocalizeController(this);
private positionBeforeCollapsing = 0;
private resizeObserver: ResizeObserver;
private size: number;

Expand Down Expand Up @@ -159,7 +161,7 @@ export default class SlSplitPanel extends ShoelaceElement {
return;
}

if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) {
if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'Enter'].includes(event.key)) {
let newPosition = this.position;
const incr = (event.shiftKey ? 10 : 1) * (this.primary === 'end' ? -1 : 1);

Expand All @@ -181,6 +183,24 @@ export default class SlSplitPanel extends ShoelaceElement {
newPosition = this.primary === 'end' ? 0 : 100;
}

// Collapse/expand the primary panel when enter is pressed
if (event.key === 'Enter') {
if (this.isCollapsed) {
newPosition = this.positionBeforeCollapsing;
this.isCollapsed = false;
} else {
const positionBeforeCollapsing = this.position;

newPosition = 0;

// Wait for position to update before setting the collapsed state
requestAnimationFrame(() => {
this.isCollapsed = true;
this.positionBeforeCollapsing = positionBeforeCollapsing;
});
}
}

this.position = clamp(newPosition, 0, 100);
}
}
Expand All @@ -206,6 +226,8 @@ export default class SlSplitPanel extends ShoelaceElement {
@watch('position')
handlePositionChange() {
this.cachedPositionInPixels = this.percentageToPixels(this.position);
this.isCollapsed = false;
this.positionBeforeCollapsing = 0;
this.positionInPixels = this.percentageToPixels(this.position);
this.emit('sl-reposition');
}
Expand Down
5 changes: 1 addition & 4 deletions src/components/tab/tab.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,8 @@ export default css`
outline: transparent;
}
:host(:focus-visible):not([disabled]) {
color: var(--sl-color-primary-600);
}
:host(:focus-visible) {
color: var(--sl-color-primary-600);
outline: var(--sl-focus-ring);
outline-offset: calc(-1 * var(--sl-focus-ring-width) - var(--sl-focus-ring-offset));
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/textarea/textarea.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ export default class SlTextarea extends ShoelaceElement implements ShoelaceFormC
this.input.style.height = 'auto';
this.input.style.height = `${this.input.scrollHeight}px`;
} else {
(this.input.style.height as string | undefined) = undefined;
this.input.style.height = '';
}
}

Expand Down
1 change: 1 addition & 0 deletions src/components/tooltip/tooltip.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export default class SlTooltip extends ShoelaceElement {
}

disconnectedCallback() {
super.disconnectedCallback();
// Cleanup this event in case the tooltip is removed while open
this.closeWatcher?.destroy();
document.removeEventListener('keydown', this.handleDocumentKeyDown);
Expand Down

0 comments on commit b3d916f

Please sign in to comment.