kopia lustrzana https://github.com/shoelace-style/shoelace
Improve Carousel Accessibility (#1218)
* fix demo * improve accessibility, reorg, and polish up * add support for up/down * fix docs * update docspull/1222/head
rodzic
0f0f71af9b
commit
8f17bf4e9d
|
@ -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
|
||||
<sl-carousel class="dynamic-carousel" pagination navigation>
|
||||
|
@ -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
|
||||
<sl-carousel class="vertical" pagination orientation="vertical">
|
||||
|
@ -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
|
||||
<sl-carousel class="scroll-hint" pagination style="--scroll-hint: 10%;">
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
`;
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
};
|
||||
|
||||
|
|
|
@ -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'
|
||||
};
|
||||
|
||||
|
|
|
@ -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'
|
||||
};
|
||||
|
||||
|
|
|
@ -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'
|
||||
};
|
||||
|
||||
|
|
|
@ -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: 'تغییر قالب رنگ'
|
||||
};
|
||||
|
||||
|
|
|
@ -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'
|
||||
};
|
||||
|
||||
|
|
|
@ -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: 'החלף פורמט צבע'
|
||||
};
|
||||
|
||||
|
|
|
@ -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'
|
||||
};
|
||||
|
||||
|
|
|
@ -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: '色のフォーマットを切り替える'
|
||||
};
|
||||
|
||||
|
|
|
@ -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'
|
||||
};
|
||||
|
||||
|
|
|
@ -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'
|
||||
};
|
||||
|
||||
|
|
|
@ -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'
|
||||
};
|
||||
|
||||
|
|
|
@ -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: 'Переключить цветовую модель'
|
||||
};
|
||||
|
||||
|
|
|
@ -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'
|
||||
};
|
||||
|
||||
|
|
|
@ -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'
|
||||
};
|
||||
|
||||
|
|
|
@ -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: '切換顏色格式'
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue