diff --git a/src/scripts/OSFramework/OSUI/Pattern/Carousel/scss/_carousel.scss b/src/scripts/OSFramework/OSUI/Pattern/Carousel/scss/_carousel.scss
index 9fd346390c..19f20f54f7 100644
--- a/src/scripts/OSFramework/OSUI/Pattern/Carousel/scss/_carousel.scss
+++ b/src/scripts/OSFramework/OSUI/Pattern/Carousel/scss/_carousel.scss
@@ -47,6 +47,12 @@
z-index: var(--layer-local-tier-1);
&__page {
+ // WCAG 2.5.8: Target offset of 6px on each side meets the 24px minimum
+ // (12px dot + 6px margin × 2 = 24px total territory per axis)
+ height: 12px;
+ margin: 6px;
+ width: 12px;
+
&.is-active {
background-color: get-background-color('primary');
z-index: var(--layer-local-tier-1);
diff --git a/src/scripts/Providers/OSUI/Carousel/Splide/Splide.ts b/src/scripts/Providers/OSUI/Carousel/Splide/Splide.ts
index d0be6f388c..55f0fd50ef 100644
--- a/src/scripts/Providers/OSUI/Carousel/Splide/Splide.ts
+++ b/src/scripts/Providers/OSUI/Carousel/Splide/Splide.ts
@@ -24,6 +24,8 @@ namespace Providers.OSUI.Carousel.Splide {
private _eventOnResize: OSFramework.OSUI.GlobalCallbacks.Generic;
// Store if a List widget is used inside the CarouselItems placeholder
private _hasList: boolean;
+ // Store the pending list-roles poll timeout id, so it can be cancelled on re-entry
+ private _listRolesPollId: number | undefined;
// Store the onSlideMoved event
private _platformEventOnSlideMoved: OSFramework.OSUI.Patterns.Carousel.Callbacks.OSOnSlideMovedEvent;
// Store initial provider options
@@ -33,6 +35,41 @@ namespace Providers.OSUI.Carousel.Splide {
super(uniqueId, new SplideConfig(configs));
}
+ // Method to apply role="list" and role="listitem" to the list element and its direct children.
+ // Skips native list elements (ul/ol) and elements whose direct children are native list
+ // elements (li/ul/ol), since those already carry implicit list semantics.
+ private _applyListRoles(listEl: HTMLElement): void {
+ const _isNativeList = listEl.tagName === 'UL' || listEl.tagName === 'OL';
+ const _hasNativeListChildren = listEl.querySelector(':scope > li, :scope > ul, :scope > ol') !== null;
+
+ if (_isNativeList || _hasNativeListChildren) {
+ return;
+ }
+
+ listEl.setAttribute('role', 'list');
+ listEl.querySelectorAll(':scope > *').forEach((item) => {
+ item.setAttribute('role', 'listitem');
+ });
+ }
+
+ // Method to wait for the OutSystems List widget to finish loading before applying roles
+ private _applyListRolesWhenReady(listEl: HTMLElement): void {
+ // Cancel any previously pending poll before starting a new one to prevent loop stacking
+ if (this._listRolesPollId !== undefined) {
+ clearTimeout(this._listRolesPollId);
+ this._listRolesPollId = undefined;
+ }
+
+ if (!listEl.classList.contains('list-loading') && listEl.children.length > 0) {
+ this._applyListRoles(listEl);
+ } else {
+ this._listRolesPollId = OSFramework.OSUI.Helper.ApplySetTimeOut(() => {
+ this._listRolesPollId = undefined;
+ this._applyListRolesWhenReady(listEl);
+ }, 100);
+ }
+ }
+
// Method to check if a List Widget is used inside the placeholder and assign the _listWidget variable
private _checkListWidget(): void {
this._hasList = OutSystems.OSUI.Utils.GetHasListInside(this._carouselPlaceholderElem);
@@ -69,8 +106,19 @@ namespace Providers.OSUI.Carousel.Splide {
// Set initial carousel width
this._setCarouselWidth();
- // Init the provider
- this.provider.mount();
+ // Init the provider — pass a custom extension so list roles are re-applied on every
+ // mount cycle, including after provider.refresh() which wipes provider.on() listeners.
+ // The extension's mount() runs after all built-in components (including A11y) have
+ // already mounted, so Splide's ARIA roles are already set and can be overridden directly.
+ this.provider.mount({
+ OSUIListRoles: () => {
+ return {
+ mount: () => {
+ this._setListRoles();
+ },
+ };
+ },
+ });
// Update pagination class, in case navigation was changed
this._togglePaginationClass();
@@ -80,13 +128,24 @@ namespace Providers.OSUI.Carousel.Splide {
private _prepareCarouselItems(): void {
// Define the element that has the items. The List widget if dynamic content, otherwise get from the placeholder directly
const _targetList = this._hasList ? this._carouselListWidgetElem : this._carouselPlaceholderElem;
- const _childrenList = _targetList.children;
+ // Snapshot into a static array: iterating the live HTMLCollection while mutating the DOM skips nodes
+ const _childrenList = Array.from(_targetList.children);
if (_childrenList.length > 0) {
// Add the placeholder content already with the correct html structure per item, expected by the library
for (const item of _childrenList) {
if (!item.classList.contains(Enum.CssClass.SplideSlide)) {
- item.classList.add(Enum.CssClass.SplideSlide);
+ // Splide assigns role="presentation" to each splide__slide element, which is then
+ // overridden with role="listitem" by the OSUIListRoles extension. Neither role
+ // is valid on an . Wrap bare images in a