diff --git a/docs/pages/resources/changelog.md b/docs/pages/resources/changelog.md index 06f9136a..d60367c8 100644 --- a/docs/pages/resources/changelog.md +++ b/docs/pages/resources/changelog.md @@ -14,13 +14,15 @@ New versions of Shoelace are released as-needed and generally occur when a criti ## Next +- Added the Arabic translation [#1852] +- Added help text to `` [#1860] +- Added help text to `` [#1800] - Fixed a bug in `` that caused HTML tags to be included in `getTextLabel()` +- Fixed a bug in `` that caused slides to not switch correctly [#1862] - Refactored component styles to be consumed more efficiently [#1692] ## 2.13.1 -- Added help text to `` -- Added help text to `` [#1800] - Fixed a bug where the safe triangle was always visible when selecting nested `` elements [#1835] ## 2.13.0 diff --git a/src/components/carousel/carousel.component.ts b/src/components/carousel/carousel.component.ts index 83743829..21f47bd7 100644 --- a/src/components/carousel/carousel.component.ts +++ b/src/components/carousel/carousel.component.ts @@ -477,7 +477,7 @@ export default class SlCarousel extends ShoelaceElement { this.scrollToSlide(nextSlide, prefersReducedMotion() ? 'auto' : behavior); } - private async scrollToSlide(slide: HTMLElement, behavior: ScrollBehavior = 'smooth') { + private scrollToSlide(slide: HTMLElement, behavior: ScrollBehavior = 'smooth') { const scrollContainer = this.scrollContainer; const scrollContainerRect = scrollContainer.getBoundingClientRect(); const nextSlideRect = slide.getBoundingClientRect(); @@ -485,16 +485,11 @@ export default class SlCarousel extends ShoelaceElement { const nextLeft = nextSlideRect.left - scrollContainerRect.left; const nextTop = nextSlideRect.top - scrollContainerRect.top; - // If the slide is already in view, don't need to scroll - if (nextLeft !== scrollContainer.scrollLeft || nextTop !== scrollContainer.scrollTop) { - scrollContainer.scrollTo({ - left: nextLeft + scrollContainer.scrollLeft, - top: nextTop + scrollContainer.scrollTop, - behavior - }); - - await waitForEvent(scrollContainer, 'scrollend'); - } + scrollContainer.scrollTo({ + left: nextLeft + scrollContainer.scrollLeft, + top: nextTop + scrollContainer.scrollTop, + behavior + }); } render() { diff --git a/src/components/carousel/carousel.test.ts b/src/components/carousel/carousel.test.ts index 42d693a7..01cfc8d1 100644 --- a/src/components/carousel/carousel.test.ts +++ b/src/components/carousel/carousel.test.ts @@ -1,6 +1,6 @@ import '../../../dist/shoelace.js'; import { clickOnElement, dragElement, moveMouseOnElement } from '../../internal/test.js'; -import { expect, fixture, html, oneEvent } from '@open-wc/testing'; +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'; @@ -8,10 +8,16 @@ import sinon from 'sinon'; import type SlCarousel from './carousel.js'; describe('', () => { + const sandbox = sinon.createSandbox(); + afterEach(async () => { await resetMouse(); }); + afterEach(() => { + sandbox.restore(); + }); + it('should render a carousel with default configuration', async () => { // Arrange const el = await fixture(html` @@ -34,15 +40,11 @@ describe('', () => { let clock: sinon.SinonFakeTimers; beforeEach(() => { - clock = sinon.useFakeTimers({ + clock = sandbox.useFakeTimers({ now: new Date() }); }); - afterEach(() => { - clock.restore(); - }); - it('should scroll forwards every `autoplay-interval` milliseconds', async () => { // Arrange const el = await fixture(html` @@ -52,7 +54,7 @@ describe('', () => { Node 3 `); - sinon.stub(el, 'next'); + sandbox.stub(el, 'next'); await el.updateComplete; @@ -73,7 +75,7 @@ describe('', () => { Node 3 `); - sinon.stub(el, 'next'); + sandbox.stub(el, 'next'); await el.updateComplete; @@ -96,7 +98,7 @@ describe('', () => { Node 3 `); - sinon.stub(el, 'next'); + sandbox.stub(el, 'next'); await el.updateComplete; @@ -183,7 +185,7 @@ describe('', () => { Node 3 `); - sinon.stub(el, 'goToSlide'); + sandbox.stub(el, 'goToSlide'); await el.updateComplete; // Act @@ -473,7 +475,7 @@ describe('', () => { `); const nextButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button--next')!; - sinon.stub(el, 'next'); + sandbox.stub(el, 'next'); await el.updateComplete; @@ -496,7 +498,7 @@ describe('', () => { `); const nextButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button--next')!; - sinon.stub(el, 'next'); + sandbox.stub(el, 'next'); el.goToSlide(2, 'auto'); await oneEvent(el.scrollContainer, 'scrollend'); @@ -560,7 +562,7 @@ describe('', () => { await el.updateComplete; const previousButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button--previous')!; - sinon.stub(el, 'previous'); + sandbox.stub(el, 'previous'); await el.updateComplete; @@ -584,7 +586,7 @@ describe('', () => { `); const previousButton: HTMLElement = el.shadowRoot!.querySelector('.carousel__navigation-button--previous')!; - sinon.stub(el, 'previous'); + sandbox.stub(el, 'previous'); await el.updateComplete; // Act @@ -632,19 +634,27 @@ describe('', () => { it('should scroll the carousel to the next slide', async () => { // Arrange const el = await fixture(html` - + Node 1 Node 2 Node 3 `); - sinon.stub(el, 'goToSlide'); - await el.updateComplete; + sandbox.spy(el, 'goToSlide'); + const expectedCarouselItem: HTMLElement = el.querySelector('sl-carousel-item:nth-child(2)')!; // Act el.next(); + await oneEvent(el.scrollContainer, 'scrollend'); + await el.updateComplete; - expect(el.goToSlide).to.have.been.calledWith(2); + const containerRect = el.scrollContainer.getBoundingClientRect(); + const itemRect = expectedCarouselItem.getBoundingClientRect(); + + // Assert + expect(el.goToSlide).to.have.been.calledWith(1); + expect(itemRect.top).to.be.equal(containerRect.top); + expect(itemRect.left).to.be.equal(containerRect.left); }); }); @@ -652,19 +662,32 @@ describe('', () => { it('should scroll the carousel to the previous slide', async () => { // Arrange const el = await fixture(html` - + Node 1 Node 2 Node 3 `); - sinon.stub(el, 'goToSlide'); - await el.updateComplete; + const expectedCarouselItem: HTMLElement = el.querySelector('sl-carousel-item:nth-child(1)')!; + + el.goToSlide(1); + + await oneEvent(el.scrollContainer, 'scrollend'); + await nextFrame(); + + sandbox.spy(el, 'goToSlide'); // Act el.previous(); + await oneEvent(el.scrollContainer, 'scrollend'); - expect(el.goToSlide).to.have.been.calledWith(-2); + const containerRect = el.scrollContainer.getBoundingClientRect(); + const itemRect = expectedCarouselItem.getBoundingClientRect(); + + // Assert + expect(el.goToSlide).to.have.been.calledWith(0); + expect(itemRect.top).to.be.equal(containerRect.top); + expect(itemRect.left).to.be.equal(containerRect.left); }); }); diff --git a/src/components/checkbox/checkbox.component.ts b/src/components/checkbox/checkbox.component.ts index ca09d4ef..903d13f0 100644 --- a/src/components/checkbox/checkbox.component.ts +++ b/src/components/checkbox/checkbox.component.ts @@ -228,6 +228,7 @@ export default class SlCheckbox extends ShoelaceElement implements ShoelaceFormC .disabled=${this.disabled} .required=${this.required} aria-checked=${this.checked ? 'true' : 'false'} + aria-describedby="help-text" @click=${this.handleClick} @input=${this.handleInput} @invalid=${this.handleInvalid} diff --git a/src/translations/ar.ts b/src/translations/ar.ts new file mode 100644 index 00000000..96352368 --- /dev/null +++ b/src/translations/ar.ts @@ -0,0 +1,41 @@ +import { registerTranslation } from '@shoelace-style/localize'; +import type { Translation } from '../utilities/localize.js'; + +const translation: Translation = { + $code: 'ar', + $name: 'العربية', + $dir: 'rtl', + + carousel: 'كاروسيل', + clearEntry: 'حذف الخيارات', + close: 'اغلاق', + copied: 'تم النسخ', + copy: 'نسخ', + currentValue: 'القيمة الحالية', + error: 'خطأ', + goToSlide: (slide, count) => `عرض شريحة رقم ${slide} من ${count}`, + hidePassword: 'اخفاء كلمة المرور', + loading: 'جاري التحميل', + nextSlide: 'الشريحة التالية', + numOptionsSelected: num => { + if (num === 0) return 'لم يتم تحديد أي خيارات'; + if (num === 1) return 'تم تحديد خيار واحد'; + if (num === 2) return 'تم تحديد خياران'; + if (num > 2 && num < 11) return `تم تحديد ${num} خيارات`; + return `تم تحديد ${num} خيار`; + }, + previousSlide: 'الشريحة السابقة', + progress: 'مقدار التقدم', + remove: 'حذف', + resize: 'تغيير الحجم', + scrollToEnd: 'الانتقال الى النهاية', + scrollToStart: 'الانتقال الى البداية', + selectAColorFromTheScreen: 'اختر لون من الشاشة', + showPassword: 'عرض كلمة المرور', + slideNum: slide => `شريحة ${slide}`, + toggleColorFormat: 'تغيير صيغة عرض اللون' +}; + +registerTranslation(translation); + +export default translation;