From e62470102260a548548444bf55590a93a27bc426 Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Fri, 3 Mar 2023 10:16:15 -0500 Subject: [PATCH 1/9] fixes #1220 --- docs/resources/changelog.md | 1 + src/components/rating/rating.test.ts | 14 ++++++++++++++ src/components/rating/rating.ts | 4 ++++ 3 files changed, 19 insertions(+) diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md index 5a25101d..cbdaafa6 100644 --- a/docs/resources/changelog.md +++ b/docs/resources/changelog.md @@ -9,6 +9,7 @@ New versions of Shoelace are released as-needed and generally occur when a criti ## Next - Added `tag__base`, `tag__content`, `tag__remove-button`, `tag__remove-button__base` parts to `` +- Fixed a bug in `` that allowed the `sl-change` event to be emitted when disabled [#1220](https://github.com/shoelace-style/shoelace/issues/1220) ## 2.2.0 diff --git a/src/components/rating/rating.test.ts b/src/components/rating/rating.test.ts index 96368792..e73217f7 100644 --- a/src/components/rating/rating.test.ts +++ b/src/components/rating/rating.test.ts @@ -79,6 +79,20 @@ describe('', () => { expect(el.value).to.equal(1); }); + it('should not emit sl-change when disabled', async () => { + const el = await fixture(html` `); + const lastSymbol = el.shadowRoot!.querySelector('.rating__symbol:last-child')!; + const changeHandler = sinon.spy(); + + el.addEventListener('sl-change', changeHandler); + + await clickOnElement(lastSymbol); + await el.updateComplete; + + expect(changeHandler).to.not.have.been.called; + expect(el.value).to.equal(5); + }); + it('should not emit sl-change when the value is changed programmatically', async () => { const el = await fixture(html` `); el.addEventListener('sl-change', () => expect.fail('sl-change incorrectly emitted')); diff --git a/src/components/rating/rating.ts b/src/components/rating/rating.ts index c77959bb..0156d51d 100644 --- a/src/components/rating/rating.ts +++ b/src/components/rating/rating.ts @@ -89,6 +89,10 @@ export default class SlRating extends ShoelaceElement { } private handleClick(event: MouseEvent) { + if (this.disabled) { + return; + } + this.setValue(this.getValueFromMousePosition(event)); this.emit('sl-change'); } From 0f0f71af9b1631a41dfe7aef07895f76c7da7893 Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Fri, 3 Mar 2023 10:36:30 -0500 Subject: [PATCH 2/9] add custom-elements.json to exports --- docs/resources/changelog.md | 1 + package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md index cbdaafa6..1faa74ca 100644 --- a/docs/resources/changelog.md +++ b/docs/resources/changelog.md @@ -8,6 +8,7 @@ New versions of Shoelace are released as-needed and generally occur when a criti ## Next +- Added `custom-elements.json` to package exports - Added `tag__base`, `tag__content`, `tag__remove-button`, `tag__remove-button__base` parts to `` - Fixed a bug in `` that allowed the `sl-change` event to be emitted when disabled [#1220](https://github.com/shoelace-style/shoelace/issues/1220) diff --git a/package.json b/package.json index e0fd47f2..9d266c11 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "types": "./dist/shoelace.d.ts", "import": "./dist/shoelace.js" }, + "./dist/custom-elements.json": "./dist/custom-elements.json", "./dist/themes/*": "./dist/themes/*", "./dist/components/*": "./dist/components/*", "./dist/utilities/*": "./dist/utilities/*", From 8f17bf4e9d1dddf8599fe0626c3bd3b0891a3439 Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Fri, 3 Mar 2023 10:53:17 -0500 Subject: [PATCH 3/9] Improve Carousel Accessibility (#1218) * fix demo * improve accessibility, reorg, and polish up * add support for up/down * fix docs * update docs --- docs/components/carousel.md | 8 +- src/components/carousel-item/carousel-item.ts | 3 +- src/components/carousel/carousel.styles.ts | 15 +- src/components/carousel/carousel.test.ts | 43 +-- src/components/carousel/carousel.ts | 300 ++++++++++-------- src/translations/da.ts | 2 + src/translations/de.ts | 2 + src/translations/en.ts | 2 + src/translations/es.ts | 2 + src/translations/fa.ts | 2 + src/translations/fr.ts | 2 + src/translations/he.ts | 2 + src/translations/hu.ts | 2 + src/translations/ja.ts | 2 + src/translations/nl.ts | 2 + src/translations/pl.ts | 2 + src/translations/pt.ts | 2 + src/translations/ru.ts | 2 + src/translations/sv.ts | 2 + src/translations/tr.ts | 2 + src/translations/zh-tw.ts | 2 + src/utilities/localize.ts | 2 + 22 files changed, 217 insertions(+), 186 deletions(-) diff --git a/docs/components/carousel.md b/docs/components/carousel.md index 113b6f86..24cfc622 100644 --- a/docs/components/carousel.md +++ b/docs/components/carousel.md @@ -533,7 +533,7 @@ const App = () => ( ### Adding and Removing Slides -The content of the carousel can be changed by appending or removing carousel items. The carousel will update itself automatically. +The content of the carousel can be changed by adding or removing carousel items. The carousel will update itself automatically. ```html preview @@ -663,9 +663,7 @@ const App = () => { ### Vertical Scrolling -Setting the `orientation` attribute to `vertical`, will make the carousel laying out vertically, making it -possible for the user to scroll it up and down. In case of heterogeneous content, for example images of different sizes, -it's important to specify a predefined height to the carousel through CSS. +Setting the `orientation` attribute to `vertical` will render the carousel in a vertical layout. If the content of your slides vary in height, you will need to set amn explicit `height` or `max-height` on the carousel using CSS. ```html preview @@ -902,7 +900,7 @@ const App = () => { ### Scroll Hint -Use the `--scroll-hint` attribute to add inline padding in horizontal carousels and block padding in vertical carousels. Setting a padding will make the closest slides slightly visible, hinting that there are more items in the carousel. +Use the `--scroll-hint` custom property to add inline padding in horizontal carousels and block padding in vertical carousels. This will make the closest slides slightly visible, hinting that there are more items in the carousel. ```html preview diff --git a/src/components/carousel-item/carousel-item.ts b/src/components/carousel-item/carousel-item.ts index ed469332..eda5ea73 100644 --- a/src/components/carousel-item/carousel-item.ts +++ b/src/components/carousel-item/carousel-item.ts @@ -25,8 +25,7 @@ export default class SlCarouselItem extends ShoelaceElement { connectedCallback() { super.connectedCallback(); - this.setAttribute('role', 'listitem'); - this.setAttribute('aria-roledescription', 'slide'); + this.setAttribute('role', 'group'); } render() { diff --git a/src/components/carousel/carousel.styles.ts b/src/components/carousel/carousel.styles.ts index f1752637..ce6d2206 100644 --- a/src/components/carousel/carousel.styles.ts +++ b/src/components/carousel/carousel.styles.ts @@ -46,6 +46,7 @@ export default css` overscroll-behavior-x: contain; scrollbar-width: none; aspect-ratio: calc(var(--aspect-ratio) * var(--slides-per-page)); + border-radius: var(--sl-border-radius-small); --slide-size: calc((100% - (var(--slides-per-page) - 1) * var(--slide-gap)) / var(--slides-per-page)); } @@ -103,7 +104,7 @@ export default css` align-items: center; background: none; border: none; - border-radius: var(--sl-border-radius-medium); + border-radius: var(--sl-border-radius-small); font-size: inherit; color: var(--sl-color-neutral-600); padding: var(--sl-spacing-x-small); @@ -140,14 +141,20 @@ export default css` width: var(--sl-spacing-small); height: var(--sl-spacing-small); background-color: var(--sl-color-neutral-300); - will-change: transform; - transition: var(--sl-transition-fast) ease-in; padding: 0; margin: 0; } .carousel__pagination-item--active { - background-color: var(--sl-color-neutral-600); + background-color: var(--sl-color-neutral-700); transform: scale(1.2); } + + /* Focus styles */ + .carousel__slides:focus-visible, + .carousel__navigation-button:focus-visible, + .carousel__pagination-item:focus-visible { + outline: var(--sl-focus-ring); + outline-offset: var(--sl-focus-ring-offset); + } `; diff --git a/src/components/carousel/carousel.test.ts b/src/components/carousel/carousel.test.ts index 1f4ca4c8..1b02a825 100644 --- a/src/components/carousel/carousel.test.ts +++ b/src/components/carousel/carousel.test.ts @@ -17,7 +17,7 @@ describe('', () => { // Assert expect(el).to.exist; expect(el).to.have.attribute('role', 'region'); - expect(el).to.have.attribute('aria-roledescription', 'carousel'); + expect(el).to.have.attribute('aria-label', 'Carousel'); expect(el.shadowRoot!.querySelector('.carousel__navigation')).not.to.exist; expect(el.shadowRoot!.querySelector('.carousel__pagination')).not.to.exist; }); @@ -539,7 +539,6 @@ describe('', () => { // Assert expect(el.scrollContainer).to.have.attribute('aria-busy', 'false'); - expect(el.scrollContainer).to.have.attribute('aria-live', 'polite'); expect(el.scrollContainer).to.have.attribute('aria-atomic', 'true'); expect(pagination).to.have.attribute('role', 'tablist'); @@ -585,45 +584,5 @@ describe('', () => { expect(el.scrollContainer).to.have.attribute('aria-busy', 'false'); }); }); - - describe('when autoplay is active', () => { - it('should disable live announcement', async () => { - // Arrange - const el = await fixture(html` - - Node 1 - Node 2 - Node 3 - - `); - - await el.updateComplete; - - // Assert - expect(el.scrollContainer).to.have.attribute('aria-live', 'off'); - }); - - describe('and user is interacting with the carousel', () => { - it('should enable live announcement', async () => { - // Arrange - const el = await fixture(html` - - Node 1 - Node 2 - Node 3 - - `); - - await el.updateComplete; - - // Act - el.dispatchEvent(new Event('focusin')); - await el.updateComplete; - - // Assert - expect(el.scrollContainer).to.have.attribute('aria-live', 'polite'); - }); - }); - }); }); }); diff --git a/src/components/carousel/carousel.ts b/src/components/carousel/carousel.ts index 417e8ce5..d8576f17 100644 --- a/src/components/carousel/carousel.ts +++ b/src/components/carousel/carousel.ts @@ -10,7 +10,6 @@ import { prefersReducedMotion } from '../../internal/animate'; import { range } from 'lit/directives/range.js'; import { ScrollController } from './scroll-controller'; import { watch } from '../../internal/watch'; -import { when } from 'lit/directives/when.js'; import ShoelaceElement from '../../internal/shoelace-element'; import SlCarouselItem from '../carousel-item/carousel-item'; import styles from './carousel.styles'; @@ -98,7 +97,7 @@ export default class SlCarousel extends ShoelaceElement { connectedCallback(): void { super.connectedCallback(); this.setAttribute('role', 'region'); - this.setAttribute('aria-roledescription', 'carousel'); + this.setAttribute('aria-label', this.localize.term('carousel')); const intersectionObserver = new IntersectionObserver( (entries: IntersectionObserverEntry[]) => { @@ -137,71 +136,61 @@ export default class SlCarousel extends ShoelaceElement { this.mutationObserver.observe(this, { childList: true, subtree: false }); } + private getPageCount() { + return Math.ceil(this.getSlides().length / this.slidesPerPage); + } + + private getCurrentPage() { + return Math.floor(this.activeSlide / this.slidesPerPage); + } + private getSlides({ excludeClones = true }: { excludeClones?: boolean } = {}) { return [...this.slides].filter(slide => !excludeClones || !slide.hasAttribute('data-clone')); } - /** - * Move the carousel backward by `slides-per-move` slides. - * - * @param behavior - The behavior used for scrolling. - */ - previous(behavior: ScrollBehavior = 'smooth') { - this.goToSlide(this.activeSlide - this.slidesPerMove, behavior); - } + private handleKeyDown(event: KeyboardEvent) { + if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End'].includes(event.key)) { + const target = event.target as HTMLElement; + const isRtl = this.localize.dir() === 'rtl'; + const isFocusInPagination = target.closest('[part~="pagination-item"]') !== null; + const isNext = + event.key === 'ArrowDown' || (!isRtl && event.key === 'ArrowRight') || (isRtl && event.key === 'ArrowLeft'); + const isPrevious = + event.key === 'ArrowUp' || (!isRtl && event.key === 'ArrowLeft') || (isRtl && event.key === 'ArrowRight'); - /** - * Move the carousel forward by `slides-per-move` slides. - * - * @param behavior - The behavior used for scrolling. - */ - next(behavior: ScrollBehavior = 'smooth') { - this.goToSlide(this.activeSlide + this.slidesPerMove, behavior); - } + event.preventDefault(); - /** - * Scrolls the carousel to the slide specified by `index`. - * - * @param index - The slide index. - * @param behavior - The behavior used for scrolling. - */ - goToSlide(index: number, behavior: ScrollBehavior = 'smooth') { - const { slidesPerPage, loop } = this; + if (isPrevious) { + this.previous(); + } - const slides = this.getSlides(); - const slidesWithClones = this.getSlides({ excludeClones: false }); + if (isNext) { + this.next(); + } - // Sets the next index without taking into account clones, if any. - const newActiveSlide = (index + slides.length) % slides.length; - this.activeSlide = newActiveSlide; + if (event.key === 'Home') { + this.goToSlide(0); + } - // Get the index of the next slide. For looping carousel it adds `slidesPerPage` - // to normalize the starting index in order to ignore the first nth clones. - const nextSlideIndex = clamp(index + (loop ? slidesPerPage : 0), 0, slidesWithClones.length - 1); - const nextSlide = slidesWithClones[nextSlideIndex]; + if (event.key === 'End') { + this.goToSlide(this.getSlides().length - 1); + } - this.scrollContainer.scrollTo({ - left: nextSlide.offsetLeft, - top: nextSlide.offsetTop, - behavior: prefersReducedMotion() ? 'auto' : behavior - }); - } + if (isFocusInPagination) { + this.updateComplete.then(() => { + const activePaginationItem = this.shadowRoot?.querySelector( + '[part~="pagination-item--active"]' + ); - handleSlotChange(mutations: MutationRecord[]) { - const needsInitialization = mutations.some(mutation => - [...mutation.addedNodes, ...mutation.removedNodes].some( - node => SlCarouselItem.isCarouselItem(node) && !(node as HTMLElement).hasAttribute('data-clone') - ) - ); - - // Reinitialize the carousel if a carousel item has been added or removed - if (needsInitialization) { - this.initializeSlides(); - this.requestUpdate(); + if (activePaginationItem) { + activePaginationItem.focus(); + } + }); + } } } - handleScrollEnd() { + private handleScrollEnd() { const slides = this.getSlides(); const entries = [...this.intersectionObserverEntries.values()]; @@ -222,6 +211,20 @@ export default class SlCarousel extends ShoelaceElement { } } + private handleSlotChange(mutations: MutationRecord[]) { + const needsInitialization = mutations.some(mutation => + [...mutation.addedNodes, ...mutation.removedNodes].some( + node => SlCarouselItem.isCarouselItem(node) && !(node as HTMLElement).hasAttribute('data-clone') + ) + ); + + // Reinitialize the carousel if a carousel item has been added or removed + if (needsInitialization) { + this.initializeSlides(); + this.requestUpdate(); + } + } + @watch('loop', { waitUntilFirstUpdate: true }) @watch('slidesPerPage', { waitUntilFirstUpdate: true }) initializeSlides() { @@ -231,11 +234,12 @@ export default class SlCarousel extends ShoelaceElement { this.intersectionObserverEntries.clear(); // Removes all the cloned elements from the carousel - this.getSlides({ excludeClones: false }).forEach(slide => { + 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('slide_num', index + 1)); if (slide.hasAttribute('data-clone')) { slide.remove(); @@ -315,90 +319,59 @@ export default class SlCarousel extends ShoelaceElement { this.scrollController.mouseDragging = this.mouseDragging; } - private getPageCount() { - return Math.ceil(this.getSlides().length / this.slidesPerPage); + /** + * Move the carousel backward by `slides-per-move` slides. + * + * @param behavior - The behavior used for scrolling. + */ + previous(behavior: ScrollBehavior = 'smooth') { + this.goToSlide(this.activeSlide - this.slidesPerMove, behavior); } - private getCurrentPage() { - return Math.floor(this.activeSlide / this.slidesPerPage); + /** + * Move the carousel forward by `slides-per-move` slides. + * + * @param behavior - The behavior used for scrolling. + */ + next(behavior: ScrollBehavior = 'smooth') { + this.goToSlide(this.activeSlide + this.slidesPerMove, behavior); } - private renderPagination = () => { - const { slidesPerPage } = this; - const pagesCount = this.getPageCount(); - const currentPage = this.getCurrentPage(); + /** + * Scrolls the carousel to the slide specified by `index`. + * + * @param index - The slide index. + * @param behavior - The behavior used for scrolling. + */ + goToSlide(index: number, behavior: ScrollBehavior = 'smooth') { + const { slidesPerPage, loop } = this; - return html` - - `; - }; + const slides = this.getSlides(); + const slidesWithClones = this.getSlides({ excludeClones: false }); - private renderNavigation = () => { - const { loop } = this; - const pagesCount = this.getPageCount(); - const currentPage = this.getCurrentPage(); - const prevEnabled = loop || currentPage > 0; - const nextEnabled = loop || currentPage < pagesCount - 1; - const isLtr = this.localize.dir() === 'ltr'; + // Sets the next index without taking into account clones, if any. + const newActiveSlide = (index + slides.length) % slides.length; + this.activeSlide = newActiveSlide; - return html` - - `; - }; + this.scrollContainer.scrollTo({ + left: nextSlide.offsetLeft, + top: nextSlide.offsetTop, + behavior: prefersReducedMotion() ? 'auto' : behavior + }); + } render() { - const { autoplayController, scrollController } = this; + const { scrollController, slidesPerPage } = this; + const pagesCount = this.getPageCount(); + const currentPage = this.getCurrentPage(); + const prevEnabled = this.loop || currentPage > 0; + const nextEnabled = this.loop || currentPage < pagesCount - 1; + const isLtr = this.localize.dir() === 'ltr'; return html` - ${when(this.navigation, this.renderNavigation)} ${when(this.pagination, this.renderPagination)} + ${this.navigation + ? html` + + ` + : ''} + ${this.pagination + ? html` + + ` + : ''} `; } diff --git a/src/translations/da.ts b/src/translations/da.ts index 2fdf1700..73551b99 100644 --- a/src/translations/da.ts +++ b/src/translations/da.ts @@ -6,6 +6,7 @@ const translation: Translation = { $name: 'Dansk', $dir: 'ltr', + carousel: 'Karrusel', clearEntry: 'Ryd indtastning', close: 'Luk', copy: 'Kopier', @@ -27,6 +28,7 @@ const translation: Translation = { scrollToStart: 'Scroll til start', selectAColorFromTheScreen: 'Vælg en farve fra skærmen', showPassword: 'Vis adgangskode', + slide_num: slide => `Slide ${slide}`, toggleColorFormat: 'Skift farveformat' }; diff --git a/src/translations/de.ts b/src/translations/de.ts index fd8d98ee..2a8012bb 100644 --- a/src/translations/de.ts +++ b/src/translations/de.ts @@ -6,6 +6,7 @@ const translation: Translation = { $name: 'Deutsch', $dir: 'ltr', + carousel: 'Karussell', clearEntry: 'Eingabe löschen', close: 'Schließen', copy: 'Kopieren', @@ -27,6 +28,7 @@ const translation: Translation = { scrollToStart: 'Zum Anfang scrollen', selectAColorFromTheScreen: 'Wähle eine Farbe vom Bildschirm', showPassword: 'Passwort anzeigen', + slide_num: slide => `Gleiten ${slide}`, toggleColorFormat: 'Farbformat umschalten' }; diff --git a/src/translations/en.ts b/src/translations/en.ts index 8999eb22..97fba0fb 100644 --- a/src/translations/en.ts +++ b/src/translations/en.ts @@ -6,6 +6,7 @@ const translation: Translation = { $name: 'English', $dir: 'ltr', + carousel: 'Carousel', clearEntry: 'Clear entry', close: 'Close', copy: 'Copy', @@ -27,6 +28,7 @@ const translation: Translation = { scrollToStart: 'Scroll to start', selectAColorFromTheScreen: 'Select a color from the screen', showPassword: 'Show password', + slide_num: slide => `Slide ${slide}`, toggleColorFormat: 'Toggle color format' }; diff --git a/src/translations/es.ts b/src/translations/es.ts index 2c8ebb78..4662e63e 100644 --- a/src/translations/es.ts +++ b/src/translations/es.ts @@ -6,6 +6,7 @@ const translation: Translation = { $name: 'Español', $dir: 'ltr', + carousel: 'Carrusel', clearEntry: 'Borrar entrada', close: 'Cerrar', copy: 'Copiar', @@ -27,6 +28,7 @@ const translation: Translation = { scrollToStart: 'Desplazarse al inicio', selectAColorFromTheScreen: 'Seleccione un color de la pantalla', showPassword: 'Mostrar contraseña', + slide_num: slide => `Diapositiva ${slide}`, toggleColorFormat: 'Alternar formato de color' }; diff --git a/src/translations/fa.ts b/src/translations/fa.ts index 9f80d118..352e01f0 100644 --- a/src/translations/fa.ts +++ b/src/translations/fa.ts @@ -6,6 +6,7 @@ const translation: Translation = { $name: 'فارسی', $dir: 'rtl', + carousel: 'چرخ فلک', clearEntry: 'پاک کردن ورودی', close: 'بستن', copy: 'رونوشت', @@ -27,6 +28,7 @@ const translation: Translation = { scrollToStart: 'پیمایش به ابتدا', selectAColorFromTheScreen: 'انتخاب یک رنگ از صفحه نمایش', showPassword: 'نمایش رمز', + slide_num: slide => `اسلاید ${slide}`, toggleColorFormat: 'تغییر قالب رنگ' }; diff --git a/src/translations/fr.ts b/src/translations/fr.ts index c70a04d5..3f516953 100644 --- a/src/translations/fr.ts +++ b/src/translations/fr.ts @@ -6,6 +6,7 @@ const translation: Translation = { $name: 'Français', $dir: 'ltr', + carousel: 'Carrousel', clearEntry: `Effacer l'entrée`, close: 'Fermer', copy: 'Copier', @@ -27,6 +28,7 @@ const translation: Translation = { scrollToStart: `Faire défiler jusqu'au début`, selectAColorFromTheScreen: `Sélectionnez une couleur à l'écran`, showPassword: 'Montrer le mot de passe', + slide_num: slide => `Glisser ${slide}`, toggleColorFormat: 'Changer le format de couleur' }; diff --git a/src/translations/he.ts b/src/translations/he.ts index 3bac5351..a276f7ac 100644 --- a/src/translations/he.ts +++ b/src/translations/he.ts @@ -6,6 +6,7 @@ const translation: Translation = { $name: 'עברית', $dir: 'rtl', + carousel: 'קרוסלה', clearEntry: 'נקה קלט', close: 'סגור', copy: 'העתק', @@ -27,6 +28,7 @@ const translation: Translation = { scrollToStart: 'גלול להתחלה', selectAColorFromTheScreen: 'בחור צבע מהמסך', showPassword: 'הראה סיסמה', + slide_num: slide => `שקופית ${slide}`, toggleColorFormat: 'החלף פורמט צבע' }; diff --git a/src/translations/hu.ts b/src/translations/hu.ts index 76546c10..8fdbbb71 100644 --- a/src/translations/hu.ts +++ b/src/translations/hu.ts @@ -6,6 +6,7 @@ const translation: Translation = { $name: 'Magyar', $dir: 'ltr', + carousel: 'Körhinta', clearEntry: 'Bejegyzés törlése', close: 'Bezárás', copy: 'Másolás', @@ -27,6 +28,7 @@ const translation: Translation = { scrollToStart: 'Görgessen az elejére', selectAColorFromTheScreen: 'Szín választása a képernyőről', showPassword: 'Jelszó megjelenítése', + slide_num: slide => `${slide}. dia`, toggleColorFormat: 'Színformátum változtatása' }; diff --git a/src/translations/ja.ts b/src/translations/ja.ts index b1996154..44ff5d75 100644 --- a/src/translations/ja.ts +++ b/src/translations/ja.ts @@ -6,6 +6,7 @@ const translation: Translation = { $name: '日本語', $dir: 'ltr', + carousel: 'カルーセル', clearEntry: 'クリアエントリ', close: '閉じる', copy: 'コピー', @@ -27,6 +28,7 @@ const translation: Translation = { scrollToStart: '最初にスクロールする', selectAColorFromTheScreen: '画面から色を選択してください', showPassword: 'パスワードを表示', + slide_num: slide => `スライド ${slide}`, toggleColorFormat: '色のフォーマットを切り替える' }; diff --git a/src/translations/nl.ts b/src/translations/nl.ts index 30798b07..7be1058d 100644 --- a/src/translations/nl.ts +++ b/src/translations/nl.ts @@ -6,6 +6,7 @@ const translation: Translation = { $name: 'Nederlands', $dir: 'ltr', + carousel: 'Carrousel', clearEntry: 'Invoer wissen', close: 'Sluiten', copy: 'Kopiëren', @@ -27,6 +28,7 @@ const translation: Translation = { scrollToStart: 'Scroll naar begin', selectAColorFromTheScreen: 'Selecteer een kleur van het scherm', showPassword: 'Laat wachtwoord zien', + slide_num: slide => `Schuif ${slide}`, toggleColorFormat: 'Wissel kleurnotatie' }; diff --git a/src/translations/pl.ts b/src/translations/pl.ts index 79d1938d..fcae2f10 100644 --- a/src/translations/pl.ts +++ b/src/translations/pl.ts @@ -6,6 +6,7 @@ const translation: Translation = { $name: 'Polski', $dir: 'ltr', + carousel: 'Karuzela', clearEntry: 'Wyczyść wpis', close: 'Zamknij', copy: 'Kopiuj', @@ -27,6 +28,7 @@ const translation: Translation = { scrollToStart: 'Przewiń do początku', selectAColorFromTheScreen: 'Próbkuj z ekranu', showPassword: 'Pokaż hasło', + slide_num: slide => `Slajd ${slide}`, toggleColorFormat: 'Przełącz format' }; diff --git a/src/translations/pt.ts b/src/translations/pt.ts index 09595942..be13cab9 100644 --- a/src/translations/pt.ts +++ b/src/translations/pt.ts @@ -6,6 +6,7 @@ const translation: Translation = { $name: 'Português', $dir: 'ltr', + carousel: 'Carrossel', clearEntry: 'Limpar entrada', close: 'Fechar', copy: 'Copiar', @@ -27,6 +28,7 @@ const translation: Translation = { scrollToStart: 'Rolar até o começo', selectAColorFromTheScreen: 'Selecionar uma cor da tela', showPassword: 'Mostrar senhaShow password', + slide_num: slide => `Diapositivo ${slide}`, toggleColorFormat: 'Trocar o formato de cor' }; diff --git a/src/translations/ru.ts b/src/translations/ru.ts index 39af667d..c045a13d 100644 --- a/src/translations/ru.ts +++ b/src/translations/ru.ts @@ -6,6 +6,7 @@ const translation: Translation = { $name: 'Русский', $dir: 'ltr', + carousel: 'Карусель', clearEntry: 'Очистить запись', close: 'Закрыть', copy: 'Скопировать', @@ -27,6 +28,7 @@ const translation: Translation = { scrollToStart: 'Пролистать к началу', selectAColorFromTheScreen: 'Выберите цвет на экране', showPassword: 'Показать пароль', + slide_num: slide => `Слайд ${slide}`, toggleColorFormat: 'Переключить цветовую модель' }; diff --git a/src/translations/sv.ts b/src/translations/sv.ts index 343b5b86..9bcffe88 100644 --- a/src/translations/sv.ts +++ b/src/translations/sv.ts @@ -6,6 +6,7 @@ const translation: Translation = { $name: 'Svenska', $dir: 'ltr', + carousel: 'Karusell', clearEntry: 'Återställ val', close: 'Stäng', copy: 'Kopiera', @@ -27,6 +28,7 @@ const translation: Translation = { scrollToStart: 'Skrolla till början', selectAColorFromTheScreen: 'Välj en färg från skärmen', showPassword: 'Visa lösenord', + slide_num: slide => `Bild ${slide}`, toggleColorFormat: 'Växla färgformat' }; diff --git a/src/translations/tr.ts b/src/translations/tr.ts index 77299473..a2c87abc 100644 --- a/src/translations/tr.ts +++ b/src/translations/tr.ts @@ -6,6 +6,7 @@ const translation: Translation = { $name: 'Türkçe', $dir: 'ltr', + carousel: 'Atlıkarınca', clearEntry: 'Girişi sil', close: 'Kapat', copy: 'Kopya', @@ -27,6 +28,7 @@ const translation: Translation = { scrollToStart: 'Başa kay', selectAColorFromTheScreen: 'Ekrandan bir renk seçin', showPassword: 'Şifreyi göster', + slide_num: slide => `Slayt ${slide}`, toggleColorFormat: 'Renk biçimini değiştir' }; diff --git a/src/translations/zh-tw.ts b/src/translations/zh-tw.ts index c579f78c..b697f4eb 100644 --- a/src/translations/zh-tw.ts +++ b/src/translations/zh-tw.ts @@ -6,6 +6,7 @@ const translation: Translation = { $name: '正體中文', $dir: 'ltr', + carousel: '旋轉木馬', clearEntry: '清空', close: '關閉', copy: '複製', @@ -27,6 +28,7 @@ const translation: Translation = { scrollToStart: '捲至頁首', selectAColorFromTheScreen: '從螢幕中選擇一種顏色', showPassword: '顯示密碼', + slide_num: slide => `幻燈片 ${slide}`, toggleColorFormat: '切換顏色格式' }; diff --git a/src/utilities/localize.ts b/src/utilities/localize.ts index e6ef29a0..ff0ae421 100644 --- a/src/utilities/localize.ts +++ b/src/utilities/localize.ts @@ -13,6 +13,7 @@ export interface Translation extends DefaultTranslation { $name: string; // e.g. English, Español $dir: 'ltr' | 'rtl'; + carousel: string; clearEntry: string; close: string; copy: string; @@ -30,5 +31,6 @@ export interface Translation extends DefaultTranslation { scrollToStart: string; selectAColorFromTheScreen: string; showPassword: string; + slide_num: (slide: number) => string; toggleColorFormat: string; } From 76fd7aa28d38d9e651fae60b59b9894908087e00 Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Fri, 3 Mar 2023 10:55:53 -0500 Subject: [PATCH 4/9] trigger update immediately --- src/components/carousel/carousel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/carousel/carousel.ts b/src/components/carousel/carousel.ts index d8576f17..4ee58e04 100644 --- a/src/components/carousel/carousel.ts +++ b/src/components/carousel/carousel.ts @@ -221,8 +221,8 @@ export default class SlCarousel extends ShoelaceElement { // Reinitialize the carousel if a carousel item has been added or removed if (needsInitialization) { this.initializeSlides(); - this.requestUpdate(); } + this.requestUpdate(); } @watch('loop', { waitUntilFirstUpdate: true }) From ab9cb5f1851033a7339ec90d52c77a97000e9ef3 Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Fri, 3 Mar 2023 10:56:55 -0500 Subject: [PATCH 5/9] update changelog --- docs/resources/changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md index 1faa74ca..57a32147 100644 --- a/docs/resources/changelog.md +++ b/docs/resources/changelog.md @@ -11,6 +11,7 @@ New versions of Shoelace are released as-needed and generally occur when a criti - Added `custom-elements.json` to package exports - Added `tag__base`, `tag__content`, `tag__remove-button`, `tag__remove-button__base` parts to `` - Fixed a bug in `` that allowed the `sl-change` event to be emitted when disabled [#1220](https://github.com/shoelace-style/shoelace/issues/1220) +- Improved accessibility of `` [#1218](https://github.com/shoelace-style/shoelace/pull/1218) ## 2.2.0 From d113d13792d41dc2712706b8a49d47e0ddb85092 Mon Sep 17 00:00:00 2001 From: dhellgartner <116464099+dhellgartner@users.noreply.github.com> Date: Mon, 6 Mar 2023 14:30:01 +0100 Subject: [PATCH 6/9] Fixed the avatar tests to produce less logs (#1222) The reason for the problems is that the error event does not escape from the shadow dom. Thus it cannot be awaited for in the test Co-authored-by: Dominikus Hellgartner --- src/components/avatar/avatar.test.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/components/avatar/avatar.test.ts b/src/components/avatar/avatar.test.ts index 969b01fb..5ccefdeb 100644 --- a/src/components/avatar/avatar.test.ts +++ b/src/components/avatar/avatar.test.ts @@ -1,5 +1,4 @@ -import { expect, fixture, html, waitUntil } from '@open-wc/testing'; -import sinon from 'sinon'; +import { aTimeout, expect, fixture, html, waitUntil } from '@open-wc/testing'; import type SlAvatar from './avatar'; // The default avatar background just misses AA contrast, but the next step up is way too dark. Since avatars aren't @@ -113,23 +112,20 @@ describe('', () => { }); it('should not render the image when the image fails to load', async () => { - const errorHandler = sinon.spy(); - el = await fixture(html``); - el.addEventListener('error', errorHandler); el.image = 'bad_image'; - waitUntil(() => errorHandler.calledOnce); + await aTimeout(0); + + await waitUntil(() => el.shadowRoot!.querySelector('img') === null); expect(el.shadowRoot!.querySelector('img')).to.be.null; }); it('should show a valid image after being passed an invalid image initially', async () => { - const errorHandler = sinon.spy(); - el = await fixture(html``); - el.addEventListener('error', errorHandler); - el.image = 'bad_image'; - waitUntil(() => errorHandler.calledOnce); + + await aTimeout(0); + await waitUntil(() => el.shadowRoot!.querySelector('img') === null); el.image = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; await el.updateComplete; From 6aaf17b81afa349788af6bd3f71b0727bc503f6f Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Mon, 6 Mar 2023 17:11:39 -0500 Subject: [PATCH 7/9] fixes #1224 --- docs/resources/changelog.md | 1 + src/components/input/input.ts | 4 ++-- src/internal/shoelace-element.ts | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md index 57a32147..6637035d 100644 --- a/docs/resources/changelog.md +++ b/docs/resources/changelog.md @@ -11,6 +11,7 @@ New versions of Shoelace are released as-needed and generally occur when a criti - Added `custom-elements.json` to package exports - Added `tag__base`, `tag__content`, `tag__remove-button`, `tag__remove-button__base` parts to `` - Fixed a bug in `` that allowed the `sl-change` event to be emitted when disabled [#1220](https://github.com/shoelace-style/shoelace/issues/1220) +- Fixed a regression in `` that caused `min` and `max` to stop working when `type="date"` [#1224](https://github.com/shoelace-style/shoelace/issues/1224) - Improved accessibility of `` [#1218](https://github.com/shoelace-style/shoelace/pull/1218) ## 2.2.0 diff --git a/src/components/input/input.ts b/src/components/input/input.ts index d56b0855..b0825ef8 100644 --- a/src/components/input/input.ts +++ b/src/components/input/input.ts @@ -144,10 +144,10 @@ export default class SlInput extends ShoelaceElement implements ShoelaceFormCont @property({ type: Number }) maxlength: number; /** The input's minimum value. Only applies to date and number input types. */ - @property({ type: Number }) min: number; + @property() min: number | string; /** The input's maximum value. Only applies to date and number input types. */ - @property({ type: Number }) max: number; + @property() max: number | string; /** * Specifies the granularity that the value must adhere to, or the special value `any` which means no stepping is diff --git a/src/internal/shoelace-element.ts b/src/internal/shoelace-element.ts index 87aae8be..962dcef9 100644 --- a/src/internal/shoelace-element.ts +++ b/src/internal/shoelace-element.ts @@ -105,8 +105,8 @@ export interface ShoelaceFormControl extends ShoelaceElement { // Constraint validation attributes pattern?: string; - min?: number | Date; - max?: number | Date; + min?: number | string | Date; + max?: number | string | Date; step?: number | 'any'; required?: boolean; minlength?: number; From f2177dccaf6b0f9825200d9b294bdfd2de632193 Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Tue, 7 Mar 2023 11:03:03 -0500 Subject: [PATCH 8/9] closes #1226 --- docs/resources/changelog.md | 1 + src/components/option/option.test.ts | 10 ++++++++++ src/components/option/option.ts | 6 ++++++ 3 files changed, 17 insertions(+) diff --git a/docs/resources/changelog.md b/docs/resources/changelog.md index 6637035d..5d6c4584 100644 --- a/docs/resources/changelog.md +++ b/docs/resources/changelog.md @@ -13,6 +13,7 @@ New versions of Shoelace are released as-needed and generally occur when a criti - Fixed a bug in `` that allowed the `sl-change` event to be emitted when disabled [#1220](https://github.com/shoelace-style/shoelace/issues/1220) - Fixed a regression in `` that caused `min` and `max` to stop working when `type="date"` [#1224](https://github.com/shoelace-style/shoelace/issues/1224) - Improved accessibility of `` [#1218](https://github.com/shoelace-style/shoelace/pull/1218) +- Improved `` so it converts non-string values to strings for convenience [#1226](https://github.com/shoelace-style/shoelace/issues/1226) ## 2.2.0 diff --git a/src/components/option/option.test.ts b/src/components/option/option.test.ts index f1cf464e..11661f40 100644 --- a/src/components/option/option.test.ts +++ b/src/components/option/option.test.ts @@ -41,4 +41,14 @@ describe('', () => { expect(slotChangeHandler).to.have.been.calledOnce; }); + + it('should convert non-string values to string', async () => { + const el = await fixture(html` Text `); + + // @ts-expect-error - intentional + el.value = 10; + await el.updateComplete; + + expect(el.value).to.equal('10'); + }); }); diff --git a/src/components/option/option.ts b/src/components/option/option.ts index 9b43e498..1cfccd14 100644 --- a/src/components/option/option.ts +++ b/src/components/option/option.ts @@ -92,6 +92,12 @@ export default class SlOption extends ShoelaceElement { @watch('value') handleValueChange() { + // Ensure the value is a string. This ensures the next line doesn't error and allows framework users to pass numbers + // instead of requiring them to cast the value to a string. + if (typeof this.value !== 'string') { + this.value = String(this.value); + } + if (this.value.includes(' ')) { console.error(`Option values cannot include a space. All spaces have been replaced with underscores.`, this); this.value = this.value.replace(/ /g, '_'); From 17ee89a5e88544ee899ce3c5d4990fe749758699 Mon Sep 17 00:00:00 2001 From: Cory LaViska Date: Tue, 7 Mar 2023 13:23:02 -0500 Subject: [PATCH 9/9] rename variable for clarity --- src/internal/form.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/internal/form.ts b/src/internal/form.ts index 25d6c8e0..667431e5 100644 --- a/src/internal/form.ts +++ b/src/internal/form.ts @@ -271,7 +271,7 @@ export class FormControlController implements ReactiveController { el.requestUpdate(); } - private doAction(type: 'submit' | 'reset', invoker?: HTMLInputElement | SlButton) { + private doAction(type: 'submit' | 'reset', submitter?: HTMLInputElement | SlButton) { if (this.form) { const button = document.createElement('button'); button.type = type; @@ -283,13 +283,13 @@ export class FormControlController implements ReactiveController { button.style.whiteSpace = 'nowrap'; // Pass name, value, and form attributes through to the temporary button - if (invoker) { - button.name = invoker.name; - button.value = invoker.value; + if (submitter) { + button.name = submitter.name; + button.value = submitter.value; ['formaction', 'formenctype', 'formmethod', 'formnovalidate', 'formtarget'].forEach(attr => { - if (invoker.hasAttribute(attr)) { - button.setAttribute(attr, invoker.getAttribute(attr)!); + if (submitter.hasAttribute(attr)) { + button.setAttribute(attr, submitter.getAttribute(attr)!); } }); } @@ -306,15 +306,15 @@ export class FormControlController implements ReactiveController { } /** Resets the form, restoring all the control to their default value */ - reset(invoker?: HTMLInputElement | SlButton) { - this.doAction('reset', invoker); + reset(submitter?: HTMLInputElement | SlButton) { + this.doAction('reset', submitter); } /** Submits the form, triggering validation and form data injection. */ - submit(invoker?: HTMLInputElement | SlButton) { + submit(submitter?: HTMLInputElement | SlButton) { // Calling form.submit() bypasses the submit event and constraint validation. To prevent this, we can inject a // native submit button into the form, "click" it, then remove it to simulate a standard form submission. - this.doAction('submit', invoker); + this.doAction('submit', submitter); } /**