Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions components/parts/common/swiper-controls.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

/**
* Swiper Controls
* Swiper add/replace automatically the aria-label, the role, the icon on the buttons
*
* @var array $args
*/

?>

<div class="swiper__controls">
<div class="swiper__buttons">
<div class="swiper-button-prev swiper__button" role="button" aria-label="<?php esc_attr_e( 'Previous slide', 'beapi-frontend-framework' ); ?>"></div>
<div class="swiper-button-next swiper__button" role="button" aria-label="<?php esc_attr_e( 'Next slide', 'beapi-frontend-framework' ); ?>"></div>
</div>
</div>
8 changes: 7 additions & 1 deletion inc/Services/Assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public function register_assets(): void {
$this->assets_tools->register_script(
'scripts',
'dist/' . $file,
array_merge( [ 'jquery' ], $asset_data['dependencies'] ), // ensure jQuery dependency is set even if not declared explicitly in the JS
array_merge( [ 'jquery', 'wp-i18n' ], $asset_data['dependencies'] ), // ensure jQuery dependency is set even if not declared explicitly in the JS
$asset_data['version'],
[ 'strategy' => 'defer' ]
);
Expand All @@ -88,6 +88,12 @@ public function register_assets(): void {
public function enqueue_scripts(): void {
// JavaScript
$this->assets_tools->enqueue_script( 'scripts' );

wp_set_script_translations(
'scripts',
'beapi-frontend-framework',
\get_theme_file_path( 'languages' )
);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"translation-revision-date":"2026-05-07 16:41+0200","generator":"WP-CLI\/2.12.0","source":"dist\/app.js","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","lang":"fr","plural-forms":"nplurals=2; plural=(n > 1);"},"Previous slide":["Diapositive pr\u00e9c\u00e9dente"],"Next slide":["Diapositive suivante"],"This is the first slide":["C'est la premi\u00e8re diapositive"],"This is the last slide":["C'est la derni\u00e8re diapositive"],"Go to slide {{index}}":["Aller \u00e0 la diapositive {{index}}"],"{{index}} of {{slidesLength}}":["{{index}} sur {{slidesLength}}"]}}}
26 changes: 26 additions & 0 deletions languages/beapi-frontend-framework.pot
Original file line number Diff line number Diff line change
Expand Up @@ -252,3 +252,29 @@ msgstr ""
msgctxt "Space size name"
msgid "lg (64px)"
msgstr ""

#: components/parts/common/swiper-controls.php:14
#: dist/app.js
msgid "Previous slide"
msgstr ""

#: components/parts/common/swiper-controls.php:15
#: dist/app.js
msgid "Next slide"
msgstr ""

#: dist/app.js
msgid "This is the first slide"
msgstr ""

#: dist/app.js
msgid "This is the last slide"
msgstr ""

#: dist/app.js
msgid "Go to slide {{index}}"
msgstr ""

#: dist/app.js
msgid "{{index}} of {{slidesLength}}"
msgstr ""
Binary file modified languages/fr_FR.mo
Binary file not shown.
26 changes: 26 additions & 0 deletions languages/fr_FR.po
Original file line number Diff line number Diff line change
Expand Up @@ -296,3 +296,29 @@ msgstr ""

#~ msgid "Toggle menu"
#~ msgstr "Ouvrir/Fermer le menu"

#: components/parts/common/swiper-controls.php:14
#: dist/app.js
msgid "Previous slide"
msgstr "Diapositive précédente"

#: components/parts/common/swiper-controls.php:15
#: dist/app.js
msgid "Next slide"
msgstr "Diapositive suivante"

#: dist/app.js
msgid "This is the first slide"
msgstr "C'est la première diapositive"

#: dist/app.js
msgid "This is the last slide"
msgstr "C'est la dernière diapositive"

#: dist/app.js
msgid "Go to slide {{index}}"
msgstr "Aller à la diapositive {{index}}"

#: dist/app.js
msgid "{{index}} of {{slidesLength}}"
msgstr "{{index}} sur {{slidesLength}}"
23 changes: 22 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@
"@fontsource-utils/scss": "^0.2.4",
"@fontsource/poppins": "^5.3.0",
"lazysizes": "^5.3.2",
"oneloop.js": "^5.3.1"
"oneloop.js": "^5.3.1",
"swiper": "^14.3.0"
},
"devDependencies": {
"@wordpress/blocks": "^11.21.0",
"@wordpress/browserslist-config": "^6.53.0",
"@wordpress/dependency-extraction-webpack-plugin": "^6.53.0",
"@wordpress/dom-ready": "^3.58.0",
"@wordpress/i18n": "^4.58.0",
"@wordpress/hooks": "^3.58.0",
"@wordpress/stylelint-config": "^24.2.0",
"clean-webpack-plugin": "^4.0.0",
Expand Down
196 changes: 196 additions & 0 deletions src/js/classes/Slider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import AbstractDomElement from './AbstractDomElement'
import Swiper from 'swiper'
import { A11y, Navigation } from 'swiper/modules'
import { __ } from '@wordpress/i18n'
import noop from '../utils/noop'

/**
* Thin wrapper around Swiper with project defaults (A11y + Navigation).
*
* Default modules are always kept; extra modules in `options.modules` are appended
* without duplicates.
*
* - A11y messages are translated via `@wordpress/i18n`; override per instance via `options.a11y` (`prevSlideMessage`, `nextSlideMessage`, etc.).
* - If slides are `<li>`, `slideRole` is cleared so Swiper does not set `role="group"` and break list semantics for screen readers (overridable via `options.a11y`).
* - When `hideNavigationFromA11y` is true, prev/next are removed from the accessibility tree and `.swiper-notification` is removed (nav announcements are unused).
* - When `hideNavigationFromA11y` is false, `.swiper-notification` is kept so Swiper can announce first/prev/next/last messages on keyboard activation of the nav controls.
* - `wrapperLiveRegion` is left to Swiper’s default (`true`): `aria-live="polite"` without autoplay, `aria-live="off"` with autoplay (overridable via `options.a11y`).
*
* Markup (minimal):
* ```html
* <div class="swiper">
* <div class="swiper-wrapper">
* <div class="swiper-slide">…</div>
* </div>
* <!-- get_template_part('components/parts/common/swiper', 'controls') -->
* </div>
* ```
*
* @example
* import Slider from './classes/Slider'
* import { Pagination } from 'swiper/modules'
*
* Slider.init('.my-slider', {
* hideNavigationFromA11y: true,
* fixLastSlideActiveOnEnd: true,
* onChange() {},
* options: {
* slidesPerView: 'auto',
* modules: [Pagination],
* navigation: {
* prevEl: '.swiper-button-prev',
* nextEl: '.swiper-button-next',
* },
* a11y: {
* // Optional: override translated defaults per slider
* nextSlideMessage: 'Next news item',
* },
* },
* })
*
* const slider = new Slider(el, { … })
* slider.getInstance() // Swiper
*
* @extends AbstractDomElement
*/
export default class Slider extends AbstractDomElement {
/**
* @param {HTMLElement} element - Root `.swiper` element
* @param {SliderOptions} [options]
*/
constructor(element, options) {
const instance = super(element, options)

// avoid double init :
if (!instance.isNewInstance()) {
return instance
}

this.init()
}

init() {
const el = this._element
const { options, onChange, hideNavigationFromA11y, fixLastSlideActiveOnEnd } = this._settings

// Merge default modules with user modules, avoiding duplicates.
// AbstractDomElement.extend() replaces arrays instead of concatenating:
// - If user passes no modules: extend copies defaults → old merge duplicated them
// - If user passes modules: extend replaces defaults → we lose A11y/Navigation
// This merge ensures defaults are always present + user modules added without duplicates.
const defaultModules = Slider.defaults.options.modules
const userModules = options.modules || []
const allModules = [...defaultModules, ...userModules.filter((mod) => !defaultModules.includes(mod))]

// role="group" on <li> breaks list semantics for screen readers.
const slidesAreListItems = el.querySelector('.swiper-slide')?.tagName === 'LI'

// Temesis feedback: hide navigation buttons from accessibility tree
// since slides are already keyboard accessible (e.g. cards with focusable content).
const hideNavigationButtons = (swiper) => {
if (!hideNavigationFromA11y) {
return
}

swiper.navigation.prevEl?.setAttribute('tabindex', '-1')
swiper.navigation.prevEl?.setAttribute('aria-hidden', 'true')
swiper.navigation.nextEl?.setAttribute('tabindex', '-1')
swiper.navigation.nextEl?.setAttribute('aria-hidden', 'true')
}

// Swiper usually sets .swiper-slide-active correctly for integer or half
// slidesPerView (e.g. 2, 2.5). With 'auto' or other fractions (e.g. 2.75),
// the last slide may not get that class when reaching the end.
// Keeping activeIndex in sync also keeps keyboard focus consistent: focusing
// content in the last slide scrolls it fully into view instead of leaving it cropped.
const applyLastSlideActiveFix = (swiper) => {
// When all slides fit, Swiper keeps isBeginning and isEnd true while activeIndex stays 0.
if (!swiper.isEnd || swiper.isBeginning) {
return
}

// Force the index on the last slide
swiper.activeIndex = swiper.slides.length - 1
// Ask Swiper to update the classes (.swiper-slide-active)
swiper.updateSlidesClasses()
}

const mergedOptions = {
...options,
modules: allModules,
a11y: {
...(slidesAreListItems ? { slideRole: '' } : {}),
...options.a11y,
},
on: {
...options.on,
afterInit: (swiper) => {
hideNavigationButtons(swiper)

// Live region announces nav messages on keyboard prev/next; keep it when
// navigation is exposed to AT, remove it when nav is intentionally hidden.
if (hideNavigationFromA11y) {
swiper.el.querySelector('.swiper-notification')?.remove()
}

options.on?.afterInit?.(swiper)
},
// Swiper resets tabindex on slide change, so we need to hide buttons again
transitionEnd: (swiper) => {
hideNavigationButtons(swiper)
options.on?.transitionEnd?.(swiper)
},
progress: function (swiper, progress) {
if (fixLastSlideActiveOnEnd) {
applyLastSlideActiveFix(swiper)
}

options.on?.progress?.call(this, swiper, progress)
},
slideChange: function (swiper) {
if (fixLastSlideActiveOnEnd) {
applyLastSlideActiveFix(swiper)
}

options.on?.slideChange?.call(this, swiper)
},
},
}

this.swiper = new Swiper(el, mergedOptions)

this.swiper.on('slideChange', onChange.bind(this))
}

/**
* @returns {import('swiper').Swiper}
*/
getInstance() {
return this.swiper
}
}

/**
* @typedef {Object} SliderOptions
* @property {Function} [onChange] - Called on slideChange (`this` = Slider instance)
* @property {boolean} [hideNavigationFromA11y=false] - Hide prev/next from AT when slides are already focusable; also removes `.swiper-notification`
* @property {boolean} [fixLastSlideActiveOnEnd=false] - Fix `.swiper-slide-active` at the end with `slidesPerView: 'auto'` or fractional values other than halves (e.g. 2.75)
* @property {import('swiper').SwiperOptions} [options] - Native Swiper options (merged with defaults)
*/

Slider.defaults = {
onChange: noop,
hideNavigationFromA11y: false,
fixLastSlideActiveOnEnd: false,
options: {
modules: [A11y, Navigation],
a11y: {
prevSlideMessage: __('Previous slide', 'beapi-frontend-framework'),
nextSlideMessage: __('Next slide', 'beapi-frontend-framework'),
firstSlideMessage: __('This is the first slide', 'beapi-frontend-framework'),
lastSlideMessage: __('This is the last slide', 'beapi-frontend-framework'),
paginationBulletMessage: __('Go to slide {{index}}', 'beapi-frontend-framework'),
slideLabelMessage: __('{{index}} of {{slidesLength}}', 'beapi-frontend-framework'),
},
},
}
Loading
Loading