diff --git a/cypress/integration/modules/a11y.js b/cypress/integration/modules/a11y.js index 5db0ea497..9ecd1df9a 100644 --- a/cypress/integration/modules/a11y.js +++ b/cypress/integration/modules/a11y.js @@ -14,6 +14,16 @@ context('Core', () => { cy.getSliderWrapper().should('have.attr', 'aria-live', 'polite'); }); + it('paginationBulletMessage', () => { + cy.initSwiper({ + pagination: true, + a11y: { paginationBulletMessage: 'Slide to {{index}}' }, + }); + cy.getPaginationBullet(1).should('have.attr', 'aria-label', 'Slide to 2'); + cy.getPaginationBullet(4).should('have.attr', 'aria-label', 'Slide to 5'); + cy.getPaginationBullet(9).should('have.attr', 'aria-label', 'Slide to 10'); + }); + it('should add aria-role-description="slide" to swiper-slide', () => { cy.initSwiper({ a11y: { itemRoleDescriptionMessage: 'test' }, @@ -21,6 +31,15 @@ context('Core', () => { cy.getSlides().should('have.attr', 'aria-role-description', 'test'); }); + it('should add aria-label="1 of 10" to swiper-slide', () => { + cy.initSwiper({ + a11y: { slideLabelMessage: '{{index}} of {{slidesLength}}' }, + }); + cy.getSlide(0).should('have.attr', 'aria-label', '1 of 10'); + cy.getSlide(4).should('have.attr', 'aria-label', '5 of 10'); + cy.getSlide(9).should('have.attr', 'aria-label', '10 of 10'); + }); + it('should add aria-role-description="slide" to swiper-container', () => { cy.initSwiper({ a11y: { containerRoleDescriptionMessage: 'test' }, diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 7ea4d262d..618b23e75 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -30,6 +30,7 @@ Cypress.Commands.add('getSliderContainer', { prevSubject: 'optional' }, () => { Cypress.Commands.add('getSlide', { prevSubject: 'optional' }, (subject, slideIndex) => { return cy.get(`.swiper-slide:nth-child(${slideIndex + 1})`); }); + Cypress.Commands.add('getSlideContains', { prevSubject: 'optional' }, (subject, content) => { cy.get('.swiper-container').contains(content); }); @@ -40,6 +41,10 @@ Cypress.Commands.add('swiperPage', { prevSubject: 'optional' }, () => { return cy.visit('cypress/test.html'); }); +Cypress.Commands.add('getPaginationBullet', { prevSubject: 'optional' }, (subject, bulletIndex) => { + return cy.get(`.swiper-pagination-bullet:nth-child(${bulletIndex + 1})`); +}); + Cypress.Commands.add( 'initSwiper', { prevSubject: 'optional' }, diff --git a/playground/angular/src/app/app.component.html b/playground/angular/src/app/app.component.html index a09ec7389..2e09eaccd 100644 --- a/playground/angular/src/app/app.component.html +++ b/playground/angular/src/app/app.component.html @@ -5,7 +5,7 @@ [slidesPerView]="3" [spaceBetween]="50" [pagination]="{ type: 'fraction' }" - virtual + [virtual]="true" slideActiveClass="swiper-active whyWouldIuseCustomClass" [centeredSlides]="true" navigation @@ -16,6 +16,22 @@ Slide Slide + + + Slide {{ slide }} + + + @@ -73,87 +89,89 @@ {{ exampleConfig | json }} + +
{{ i }} - {{ slide }} - - + +
- - Slide 1 - Slide 2 - Slide 3 - Slide 4 - Slide 5 - Slide 6 - - - Slide 1 - Slide 2 - Slide 3 - Slide 4 - Slide 5 - Slide 6 - -
-
- - Slide 1 - Slide 2 - Slide 3 - Slide 4 - Slide 5 - Slide 6 - - - Slide 1 - Slide 2 - Slide 3 - Slide 4 - Slide 5 - Slide 6 - -
+ + Slide 1 + Slide 2 + Slide 3 + Slide 4 + Slide 5 + Slide 6 + + + Slide 1 + Slide 2 + Slide 3 + Slide 4 + Slide 5 + Slide 6 + + +
+ + Slide 1 + Slide 2 + Slide 3 + Slide 4 + Slide 5 + Slide 6 + + + Slide 1 + Slide 2 + Slide 3 + Slide 4 + Slide 5 + Slide 6 + +
diff --git a/playground/angular/src/app/app.component.ts b/playground/angular/src/app/app.component.ts index 749f33cea..43e473bda 100644 --- a/playground/angular/src/app/app.component.ts +++ b/playground/angular/src/app/app.component.ts @@ -1,4 +1,5 @@ import { ChangeDetectorRef, Component, ViewChild } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; import { SwiperComponent } from 'src/angular/src/public-api'; import SwiperCore, { Navigation, @@ -34,9 +35,14 @@ export class AppComponent { show: boolean; thumbs: any; + slides$ = new BehaviorSubject(['']); constructor(private cd: ChangeDetectorRef) {} ngOnInit() {} + getSlides() { + this.slides$.next(Array.from({ length: 600 }).map((el, index) => `Slide ${index + 1}`)); + } + thumbsSwiper: any; setThumbsSwiper(swiper) { this.thumbsSwiper = swiper; diff --git a/src/angular/src/swiper.component.ts b/src/angular/src/swiper.component.ts index 1c666883e..bc821162d 100644 --- a/src/angular/src/swiper.component.ts +++ b/src/angular/src/swiper.component.ts @@ -164,7 +164,10 @@ export class SwiperComponent implements OnInit { }); if ( typeof this._navigation !== 'boolean' && - (typeof this._navigation?.nextEl === 'string' || typeof this._navigation?.prevEl === 'string') + (typeof this._navigation?.nextEl === 'string' || + typeof this._navigation?.prevEl === 'string' || + typeof this._navigation?.nextEl === 'object' || + typeof this._navigation?.prevEl === 'object') ) { this.showNavigation = false; } @@ -181,7 +184,10 @@ export class SwiperComponent implements OnInit { this._pagination = setProperty(val, { el: current || null, }); - if (typeof this._pagination !== 'boolean' && typeof this._pagination?.el === 'string') { + if ( + typeof this._pagination !== 'boolean' && + (typeof this._pagination?.el === 'string' || typeof this._pagination?.el === 'object') + ) { this.showPagination = false; } } @@ -197,7 +203,10 @@ export class SwiperComponent implements OnInit { this._scrollbar = setProperty(val, { el: current || null, }); - if (typeof this._scrollbar !== 'boolean' && typeof this._scrollbar?.el === 'string') { + if ( + typeof this._scrollbar !== 'boolean' && + (typeof this._scrollbar?.el === 'string' || typeof this._scrollbar?.el === 'object') + ) { this.showScrollbar = false; } } @@ -471,6 +480,11 @@ export class SwiperComponent implements OnInit { if (!this.virtual) { this.prependSlides = of(this.slides.slice(this.slides.length - this.loopedSlides)); this.appendSlides = of(this.slides.slice(0, this.loopedSlides)); + } else if (this.swiperRef && this.swiperRef.virtual) { + this._ngZone.runOutsideAngular(() => { + this.swiperRef.virtual.slides = this.slides; + this.swiperRef.virtual.update(true); + }); } this._changeDetectorRef.detectChanges(); }; diff --git a/src/angular/src/utils/utils.ts b/src/angular/src/utils/utils.ts index 7f31f4096..5f2452fd4 100644 --- a/src/angular/src/utils/utils.ts +++ b/src/angular/src/utils/utils.ts @@ -1,5 +1,10 @@ export function isObject(o) { - return typeof o === 'object' && o !== null && o.constructor && o.constructor === Object; + return ( + typeof o === 'object' && + o !== null && + o.constructor && + Object.prototype.toString.call(o).slice(8, -1) === 'Object' + ); } export function extend(target, src) { diff --git a/src/components/a11y/a11y.js b/src/components/a11y/a11y.js index 801bdc0ae..8833cf3f3 100644 --- a/src/components/a11y/a11y.js +++ b/src/components/a11y/a11y.js @@ -167,7 +167,10 @@ const A11y = { swiper.a11y.addElRole($(swiper.slides), 'group'); swiper.slides.each((slideEl) => { const $slideEl = $(slideEl); - swiper.a11y.addElLabel($slideEl, `${$slideEl.index() + 1} / ${swiper.slides.length}`); + const ariaLabelMessage = params.slideLabelMessage + .replace(/\{\{index\}\}/, $slideEl.index() + 1) + .replace(/\{\{slidesLength\}\}/, swiper.slides.length); + swiper.a11y.addElLabel($slideEl, ariaLabelMessage); }); // Navigation @@ -259,6 +262,7 @@ export default { firstSlideMessage: 'This is the first slide', lastSlideMessage: 'This is the last slide', paginationBulletMessage: 'Go to slide {{index}}', + slideLabelMessage: '{{index}} / {{slidesLength}}', containerMessage: null, containerRoleDescriptionMessage: null, itemRoleDescriptionMessage: null, diff --git a/src/components/core/core-class.js b/src/components/core/core-class.js index 1e5692163..f8f22b80d 100644 --- a/src/components/core/core-class.js +++ b/src/components/core/core-class.js @@ -49,7 +49,11 @@ class Swiper { constructor(...args) { let el; let params; - if (args.length === 1 && args[0].constructor && args[0].constructor === Object) { + if ( + args.length === 1 && + args[0].constructor && + Object.prototype.toString.call(args[0]).slice(8, -1) === 'Object' + ) { params = args[0]; } else { [el, params] = args; diff --git a/src/components/core/update/updateSlides.js b/src/components/core/update/updateSlides.js index 18190feff..bf5b35c02 100644 --- a/src/components/core/update/updateSlides.js +++ b/src/components/core/update/updateSlides.js @@ -167,7 +167,7 @@ export default function updateSlides() { const paddingRight = getDirectionPropertyValue(slideStyles, 'padding-right'); const marginLeft = getDirectionPropertyValue(slideStyles, 'margin-left'); const marginRight = getDirectionPropertyValue(slideStyles, 'margin-right'); - const boxSizing = slideStyles.getPropertyValue(slideStyles, 'box-sizing'); + const boxSizing = slideStyles.getPropertyValue('box-sizing'); if (boxSizing && boxSizing === 'border-box') { slideSize = width + marginLeft + marginRight; } else { diff --git a/src/components/keyboard/keyboard.js b/src/components/keyboard/keyboard.js index 94df48a60..9637a6894 100644 --- a/src/components/keyboard/keyboard.js +++ b/src/components/keyboard/keyboard.js @@ -56,15 +56,19 @@ const Keyboard = { ) { return undefined; } + + const $el = swiper.$el; + const swiperWidth = $el[0].clientWidth; + const swiperHeight = $el[0].clientHeight; const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; const swiperOffset = swiper.$el.offset(); if (rtl) swiperOffset.left -= swiper.$el[0].scrollLeft; const swiperCoord = [ [swiperOffset.left, swiperOffset.top], - [swiperOffset.left + swiper.width, swiperOffset.top], - [swiperOffset.left, swiperOffset.top + swiper.height], - [swiperOffset.left + swiper.width, swiperOffset.top + swiper.height], + [swiperOffset.left + swiperWidth, swiperOffset.top], + [swiperOffset.left, swiperOffset.top + swiperHeight], + [swiperOffset.left + swiperWidth, swiperOffset.top + swiperHeight], ]; for (let i = 0; i < swiperCoord.length; i += 1) { const point = swiperCoord[i]; diff --git a/src/react/utils.js b/src/react/utils.js index 7993804ff..0378af8df 100644 --- a/src/react/utils.js +++ b/src/react/utils.js @@ -1,5 +1,10 @@ function isObject(o) { - return typeof o === 'object' && o !== null && o.constructor && o.constructor === Object; + return ( + typeof o === 'object' && + o !== null && + o.constructor && + Object.prototype.toString.call(o).slice(8, -1) === 'Object' + ); } function extend(target, src) { diff --git a/src/svelte/utils.js b/src/svelte/utils.js index 7993804ff..0378af8df 100644 --- a/src/svelte/utils.js +++ b/src/svelte/utils.js @@ -1,5 +1,10 @@ function isObject(o) { - return typeof o === 'object' && o !== null && o.constructor && o.constructor === Object; + return ( + typeof o === 'object' && + o !== null && + o.constructor && + Object.prototype.toString.call(o).slice(8, -1) === 'Object' + ); } function extend(target, src) { diff --git a/src/types/components/a11y.d.ts b/src/types/components/a11y.d.ts index c73cabde3..d85f3dfc4 100644 --- a/src/types/components/a11y.d.ts +++ b/src/types/components/a11y.d.ts @@ -72,4 +72,11 @@ export interface A11yOptions { * @default null */ itemRoleDescriptionMessage?: string | null; + + /** + * Message for screen readers describing the label of slide element + * + * @default '{{index}} / {{slidesLength}}' + */ + slideLabelMessage?: string; } diff --git a/src/utils/utils.js b/src/utils/utils.js index 4134f737f..c85924d0e 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -85,7 +85,12 @@ function getTranslate(el, axis = 'x') { return curTransform || 0; } function isObject(o) { - return typeof o === 'object' && o !== null && o.constructor && o.constructor === Object; + return ( + typeof o === 'object' && + o !== null && + o.constructor && + Object.prototype.toString.call(o).slice(8, -1) === 'Object' + ); } function extend(...args) { const to = Object(args[0]); diff --git a/src/vue/utils.js b/src/vue/utils.js index e2e895d0c..a6839f045 100644 --- a/src/vue/utils.js +++ b/src/vue/utils.js @@ -1,5 +1,10 @@ function isObject(o) { - return typeof o === 'object' && o !== null && o.constructor && o.constructor === Object; + return ( + typeof o === 'object' && + o !== null && + o.constructor && + Object.prototype.toString.call(o).slice(8, -1) === 'Object' + ); } function extend(target, src) {