From 0849dc5f2b6f1357b735a1a36eeb2ccc826a4731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81dric=20Andrietti?= Date: Fri, 2 Oct 2026 11:15:06 +0200 Subject: [PATCH 1/2] feat(Slider): integrate base Swiper Class for enhanced slide navigation and accessibility - Added Swiper library for improved slider functionality. - Created a new Slider class to manage Swiper instances with accessibility features. - Implemented custom controls for previous and next slide navigation. - Updated package dependencies to include Swiper and WordPress i18n for translations. - Added French translations for slider navigation messages. - Introduced SCSS styles for Swiper controls and integrated them into the main stylesheets. --- components/parts/common/swiper-controls.php | 17 ++ inc/Services/Assets.php | 8 +- ...r_FR-a442c66c97675e8bda3e3b62b3a8c8ec.json | 1 + languages/beapi-frontend-framework.pot | 24 +++ languages/fr_FR.po | 24 +++ package-lock.json | 23 +- package.json | 4 +- src/js/classes/Slider.js | 196 ++++++++++++++++++ src/scss/10-vendor/_swiper.scss | 57 +++++ src/scss/editor.scss | 2 +- src/scss/style.scss | 2 +- 11 files changed, 353 insertions(+), 5 deletions(-) create mode 100644 components/parts/common/swiper-controls.php create mode 100644 languages/beapi-frontend-framework-fr_FR-a442c66c97675e8bda3e3b62b3a8c8ec.json create mode 100644 src/js/classes/Slider.js create mode 100644 src/scss/10-vendor/_swiper.scss diff --git a/components/parts/common/swiper-controls.php b/components/parts/common/swiper-controls.php new file mode 100644 index 00000000..f6bb1ded --- /dev/null +++ b/components/parts/common/swiper-controls.php @@ -0,0 +1,17 @@ + + +
+
+
+
+
+
diff --git a/inc/Services/Assets.php b/inc/Services/Assets.php index 474af5bb..f764a0d3 100644 --- a/inc/Services/Assets.php +++ b/inc/Services/Assets.php @@ -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' ] ); @@ -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' ) + ); } /** diff --git a/languages/beapi-frontend-framework-fr_FR-a442c66c97675e8bda3e3b62b3a8c8ec.json b/languages/beapi-frontend-framework-fr_FR-a442c66c97675e8bda3e3b62b3a8c8ec.json new file mode 100644 index 00000000..c2b80112 --- /dev/null +++ b/languages/beapi-frontend-framework-fr_FR-a442c66c97675e8bda3e3b62b3a8c8ec.json @@ -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}}"]}}} \ No newline at end of file diff --git a/languages/beapi-frontend-framework.pot b/languages/beapi-frontend-framework.pot index 6d9bc44c..63f3d611 100644 --- a/languages/beapi-frontend-framework.pot +++ b/languages/beapi-frontend-framework.pot @@ -252,3 +252,27 @@ msgstr "" msgctxt "Space size name" msgid "lg (64px)" msgstr "" + +#: dist/app.js +msgid "Previous slide" +msgstr "" + +#: 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 "" diff --git a/languages/fr_FR.po b/languages/fr_FR.po index 4f5562c0..7737bb2a 100644 --- a/languages/fr_FR.po +++ b/languages/fr_FR.po @@ -296,3 +296,27 @@ msgstr "" #~ msgid "Toggle menu" #~ msgstr "Ouvrir/Fermer le menu" + +#: dist/app.js +msgid "Previous slide" +msgstr "Diapositive précédente" + +#: 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}}" diff --git a/package-lock.json b/package-lock.json index 6edc34d7..482636c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "@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", @@ -20,6 +21,7 @@ "@wordpress/dependency-extraction-webpack-plugin": "^6.53.0", "@wordpress/dom-ready": "^3.58.0", "@wordpress/hooks": "^3.58.0", + "@wordpress/i18n": "^4.58.0", "@wordpress/stylelint-config": "^24.2.0", "clean-webpack-plugin": "^4.0.0", "concurrently": "^9.2.1", @@ -15286,6 +15288,25 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/swiper": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-14.3.0.tgz", + "integrity": "sha512-qw0Xv1+sylM8KCQxWxV2iYAK2G6XjFGPZ/8F1baEsY8DK4A3sTnLx6jUJ0G71rDbVQ05JHMWkcWmJ5NvlxZO0w==", + "funding": [ + { + "type": "custom", + "url": "https://sponsors.nolimits4web.com" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nolimits4web" + } + ], + "license": "MIT", + "engines": { + "node": ">= 4.7.0" + } + }, "node_modules/table": { "version": "6.9.0", "dev": true, diff --git a/package.json b/package.json index 3f1255d9..40b231da 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/js/classes/Slider.js b/src/js/classes/Slider.js new file mode 100644 index 00000000..67d3a5e8 --- /dev/null +++ b/src/js/classes/Slider.js @@ -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 `
  • `, `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 + *
    + *
    + *
    …
    + *
    + * + *
    + * ``` + * + * @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
  • 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'), + }, + }, +} diff --git a/src/scss/10-vendor/_swiper.scss b/src/scss/10-vendor/_swiper.scss new file mode 100644 index 00000000..e43f51d9 --- /dev/null +++ b/src/scss/10-vendor/_swiper.scss @@ -0,0 +1,57 @@ +@use "../01-abstract/variables" as *; +@use "../01-abstract/theme-json" as *; +@use "~swiper/css"; +@use "~swiper/css/a11y"; + +.swiper { + --swiper-theme-color: #{$color-primary}; + + &__controls { + display: flex; + align-items: center; + justify-content: center; + + &:has(.swiper-button-lock) { + display: none; + } + } + + &__buttons { + display: flex; + gap: $settings-spacing-xs; + + button, + [role="button"] { + &.swiper-button-prev { + transform: scaleX(-1); + } + + &.swiper-button-disabled { + cursor: not-allowed; + opacity: .3; + } + } + } + + &__button { + width: 44px; + height: 44px; + border-radius: 100%; + background-color: var(--swiper-theme-color); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + } + + &-overflow-visible { + overflow: visible; + } + + .swiper-wrapper { + box-sizing: border-box; + padding: 0; + margin: 0; + list-style: none; + } +} diff --git a/src/scss/editor.scss b/src/scss/editor.scss index 7c6fad9d..891581d9 100644 --- a/src/scss/editor.scss +++ b/src/scss/editor.scss @@ -84,4 +84,4 @@ variables.$entry-file-name: "editor"; * == Plugins or libraries custom styles. */ -// @use "10-vendor/..."; +@use "10-vendor/swiper"; diff --git a/src/scss/style.scss b/src/scss/style.scss index 486bce0a..a5f76002 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -108,4 +108,4 @@ variables.$entry-file-name: "style"; * == Plugins or libraries custom styles. */ -// @use "10-vendor/..."; +@use "10-vendor/swiper"; From beedf401548e849f8d4451785c045b15de73d692 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81dric=20Andrietti?= Date: Fri, 2 Oct 2026 11:18:29 +0200 Subject: [PATCH 2/2] fix(Slider): update accessibility labels for swiper controls - Replaced static French labels for previous and next slide buttons with translatable strings using WordPress i18n functions. - Updated language files to include the new translations for accessibility. --- components/parts/common/swiper-controls.php | 4 ++-- languages/beapi-frontend-framework.pot | 2 ++ languages/fr_FR.mo | Bin 2536 -> 2970 bytes languages/fr_FR.po | 2 ++ 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/components/parts/common/swiper-controls.php b/components/parts/common/swiper-controls.php index f6bb1ded..aa4fa4f0 100644 --- a/components/parts/common/swiper-controls.php +++ b/components/parts/common/swiper-controls.php @@ -11,7 +11,7 @@
    -
    -
    +
    +
    diff --git a/languages/beapi-frontend-framework.pot b/languages/beapi-frontend-framework.pot index 63f3d611..3f2a99c9 100644 --- a/languages/beapi-frontend-framework.pot +++ b/languages/beapi-frontend-framework.pot @@ -253,10 +253,12 @@ 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 "" diff --git a/languages/fr_FR.mo b/languages/fr_FR.mo index c62c0ed6febb0d2f2fc9642e8a33d185010b4346..da90e99ce8e6071c4535847176a6c136a9b3c061 100644 GIT binary patch delta 1186 zcmZY7%S%*Y7{~Ef9nD*2V_wQG$1WDFEQoAT5M3mYGLqU@Ff>Q!z&Io4oEZ$AW)b-h zOhFVui-;sz2|)`NHAt?aRc^vfB(-bRCW^klGp{z_%bfFhFK6EOd7g8=cRcPcuEt{z z4XvMRH`j2DF(+`gmK$xP&X{JrY8}Hi#uM0pc^tw5Uc@CkZjBkUo$)B@xp8d4No>VC z6^mw$PMnDn?!p(?iSJNsrYU4%ZXO_8j<45emHQa-L@F~^nC2Ae;c1y)&>k2BMHJrqs znBshMtI-$_KcF@~##74h5-OnUsEVXeujnqSQcv&@zQI2Hg6wJjqDtLB)Wl@ssC{}+ zfgMHdcM6MIxI$+KChP-w>wQ#dAD~M2995Y`)bpQE8+}7n;Szjjf6~Z)T`V&Y+qUP`lI)&DZNHjMpd!jZ}bf@i~>~f zYIM+NMlNB+5YLUT@BN2lsIu;8OO=P}z}`*~wM6uv8n?bPi>mFzW1Il9)4iML4!gv`u2a4{>6KslZuS0VV>#*i Pna#`pBM)-^rqInl3LLlo delta 753 zcmXxiPbkA-9LMoz_J?i$%*N*LxR4^{NGXRQSyQ7CWgzS`rU#R#36sdr%<%Bz zENbEs1~H9oxP}UF+m0V#3;kp4!W=5#ON?Lv71$F-P1cwA$j@%tFv8=UY{XFu zToaYNvp%B&dB-iRU_7pvzzb@_4EV>__qEg{t?j5V$P34<%uSER;sO)m#RCGe!YfpaJyW??+zLYcUpKvz(nS5{H#_QCA>&|Xy+$n?({{RtTKnDN- diff --git a/languages/fr_FR.po b/languages/fr_FR.po index 7737bb2a..18ace95a 100644 --- a/languages/fr_FR.po +++ b/languages/fr_FR.po @@ -297,10 +297,12 @@ 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"