Improve Carousel Accessibility (#1218)

* fix demo

* improve accessibility, reorg, and polish up

* add support for up/down

* fix docs

* update docs
This commit is contained in:
Cory LaViska
2023-03-03 10:53:17 -05:00
committed by GitHub
parent 0f0f71af9b
commit 8f17bf4e9d
22 changed files with 217 additions and 186 deletions

View File

@@ -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() {

View File

@@ -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);
}
`;

View File

@@ -17,7 +17,7 @@ describe('<sl-carousel>', () => {
// 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('<sl-carousel>', () => {
// 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('<sl-carousel>', () => {
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<SlCarousel>(html`
<sl-carousel autoplay>
<sl-carousel-item>Node 1</sl-carousel-item>
<sl-carousel-item>Node 2</sl-carousel-item>
<sl-carousel-item>Node 3</sl-carousel-item>
</sl-carousel>
`);
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<SlCarousel>(html`
<sl-carousel autoplay>
<sl-carousel-item>Node 1</sl-carousel-item>
<sl-carousel-item>Node 2</sl-carousel-item>
<sl-carousel-item>Node 3</sl-carousel-item>
</sl-carousel>
`);
await el.updateComplete;
// Act
el.dispatchEvent(new Event('focusin'));
await el.updateComplete;
// Assert
expect(el.scrollContainer).to.have.attribute('aria-live', 'polite');
});
});
});
});
});

View File

@@ -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<HTMLButtonElement>(
'[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`
<nav part="pagination" role="tablist" class="carousel__pagination" aria-controls="scroll-container">
${map(range(pagesCount), index => {
const isActive = index === currentPage;
return html`
<button
part="pagination-item ${isActive ? 'pagination-item--active' : ''}"
class="${classMap({
'carousel__pagination-item': true,
'carousel__pagination-item--active': isActive
})}"
aria-selected="${isActive ? 'true' : 'false'}"
aria-label="${this.localize.term('goToSlide', index + 1, pagesCount)}"
role="tab"
@click="${() => this.goToSlide(index * slidesPerPage)}"
></button>
`;
})}
</nav>
`;
};
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`
<nav part="navigation" class="carousel__navigation">
<button
@click="${prevEnabled ? () => this.previous() : null}"
aria-disabled="${prevEnabled ? 'false' : 'true'}"
aria-controls="scroll-container"
class="${classMap({
'carousel__navigation-button': true,
'carousel__navigation-button--previous': true,
'carousel__navigation-button--disabled': !prevEnabled
})}"
aria-label="${this.localize.term('previousSlide')}"
part="navigation-button navigation-button--previous"
>
<slot name="previous-icon">
<sl-icon library="system" name="${isLtr ? 'chevron-left' : 'chevron-right'}"></sl-icon>
</slot>
</button>
// 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];
<button
@click="${nextEnabled ? () => this.next() : null}"
aria-disabled="${nextEnabled ? 'false' : 'true'}"
aria-controls="scroll-container"
class="${classMap({
'carousel__navigation-button': true,
'carousel__navigation-button--next': true,
'carousel__navigation-button--disabled': !nextEnabled
})}"
aria-label="${this.localize.term('nextSlide')}"
part="navigation-button navigation-button--next"
>
<slot name="next-icon">
<sl-icon library="system" name="${isLtr ? 'chevron-right' : 'chevron-left'}"></sl-icon>
</slot>
</button>
</nav>
`;
};
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`
<div part="base" class="carousel">
@@ -410,18 +383,79 @@ export default class SlCarousel extends ShoelaceElement {
'carousel__slides--horizontal': this.orientation === 'horizontal',
'carousel__slides--vertical': this.orientation === 'vertical'
})}"
@scrollend="${this.handleScrollEnd}"
role="list"
tabindex="0"
style="--slides-per-page: ${this.slidesPerPage};"
aria-live="${!autoplayController.stopped && !autoplayController.paused ? 'off' : 'polite'}"
aria-busy="${scrollController.scrolling ? 'true' : 'false'}"
aria-atomic="true"
tabindex="0"
@keydown=${this.handleKeyDown}
@scrollend=${this.handleScrollEnd}
>
<slot></slot>
</div>
${when(this.navigation, this.renderNavigation)} ${when(this.pagination, this.renderPagination)}
${this.navigation
? html`
<div part="navigation" class="carousel__navigation">
<button
part="navigation-button navigation-button--previous"
class="${classMap({
'carousel__navigation-button': true,
'carousel__navigation-button--previous': true,
'carousel__navigation-button--disabled': !prevEnabled
})}"
aria-label="${this.localize.term('previousSlide')}"
aria-controls="scroll-container"
aria-disabled="${prevEnabled ? 'false' : 'true'}"
@click=${prevEnabled ? () => this.previous() : null}
>
<slot name="previous-icon">
<sl-icon library="system" name="${isLtr ? 'chevron-left' : 'chevron-right'}"></sl-icon>
</slot>
</button>
<button
part="navigation-button navigation-button--next"
class=${classMap({
'carousel__navigation-button': true,
'carousel__navigation-button--next': true,
'carousel__navigation-button--disabled': !nextEnabled
})}
aria-label="${this.localize.term('nextSlide')}"
aria-controls="scroll-container"
aria-disabled="${nextEnabled ? 'false' : 'true'}"
@click=${nextEnabled ? () => this.next() : null}
>
<slot name="next-icon">
<sl-icon library="system" name="${isLtr ? 'chevron-right' : 'chevron-left'}"></sl-icon>
</slot>
</button>
</div>
`
: ''}
${this.pagination
? html`
<div part="pagination" role="tablist" class="carousel__pagination" aria-controls="scroll-container">
${map(range(pagesCount), index => {
const isActive = index === currentPage;
return html`
<button
part="pagination-item ${isActive ? 'pagination-item--active' : ''}"
class="${classMap({
'carousel__pagination-item': true,
'carousel__pagination-item--active': isActive
})}"
role="tab"
aria-selected="${isActive ? 'true' : 'false'}"
aria-label="${this.localize.term('goToSlide', index + 1, pagesCount)}"
tabindex=${isActive ? '0' : '-1'}
@click=${() => this.goToSlide(index * slidesPerPage)}
@keydown=${this.handleKeyDown}
></button>
`;
})}
</div>
`
: ''}
</div>
`;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: 'تغییر قالب رنگ'
};

View File

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

View File

@@ -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: 'החלף פורמט צבע'
};

View File

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

View File

@@ -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: '色のフォーマットを切り替える'
};

View File

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

View File

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

View File

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

View File

@@ -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: 'Переключить цветовую модель'
};

View File

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

View File

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

View File

@@ -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: '切換顏色格式'
};

View File

@@ -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;
}