Skip to content

Commit

Permalink
feat(carousel): support w3c WAI-ARIA pattern (#1180)
Browse files Browse the repository at this point in the history
  • Loading branch information
mlmoravek authored Feb 11, 2025
1 parent bde7ab1 commit bab6b37
Show file tree
Hide file tree
Showing 18 changed files with 616 additions and 291 deletions.
83 changes: 45 additions & 38 deletions packages/docs/components/Carousel.md

Large diffs are not rendered by default.

422 changes: 240 additions & 182 deletions packages/oruga/src/components/carousel/Carousel.vue

Large diffs are not rendered by default.

20 changes: 11 additions & 9 deletions packages/oruga/src/components/carousel/CarouselItem.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<script setup lang="ts">
import { computed } from "vue";
import { getDefault } from "@/utils/config";
import { defineClasses, useProviderChild } from "@/composables";
import type { CarouselComponent } from "./types";
Expand All @@ -20,7 +19,6 @@ defineOptions({
const props = withDefaults(defineProps<CarouselItemProps>(), {
override: undefined,
clickable: false,
ariaRole: () => getDefault("carousel.ariaRole", "option"),
});
/** inject functionalities and data from the parent component */
Expand Down Expand Up @@ -51,16 +49,20 @@ const itemClasses = defineClasses(

<template>
<div
v-if="parent"
:class="itemClasses"
:style="itemStyle"
:id="`carouselpanel-${item.identifier}`"
data-oruga="carousel-item"
:data-id="`carousel-${item.identifier}`"
:role="ariaRole"
aria-roledescription="item"
:aria-selected="isActive"
:class="itemClasses"
:style="itemStyle"
:role="parent.indicators ? 'tabpanel' : 'group'"
:aria-labelledby="`carousel-${item.identifier}`"
aria-roledescription="slide"
:aria-label="`${item.index + 1} of ${parent.total}`"
draggable="true"
@click="onClick"
@keypress.enter="onClick">
@keypress.enter="onClick"
@dragstart="parent.onDrag"
@touchstart="parent.onDrag">
<!--
@slot Default content
-->
Expand Down
2 changes: 1 addition & 1 deletion packages/oruga/src/components/carousel/examples/base.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const carousels = [

<template>
<section>
<o-carousel>
<o-carousel indicator-inside>
<o-carousel-item v-for="(carousel, i) in carousels" :key="i">
<article
class="ex-slide"
Expand Down
31 changes: 23 additions & 8 deletions packages/oruga/src/components/carousel/examples/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import BaseCode from "./base.vue?raw";
import List from "./list.vue";
import ListCode from "./list.vue?raw";
import CustomIndicators from "./custom-indicators.vue";
import CustomIndicatorsCode from "./custom-indicators.vue?raw";
import Indicators from "./indicators.vue";
import IndicatorsCode from "./indicators.vue?raw";
import Customise from "./customise.vue";
import CustomiseCode from "./customise.vue?raw";
Expand All @@ -21,7 +21,8 @@ import CustomiseCode from "./customise.vue?raw";
<h3 id="list">Carousel List</h3>
<p>
The <code>items-to-show</code> and <code>items-to-list</code> props
can be used to specify the number of items to be displayed at once.
can be used to specify the number of items to be displayed and
rotated at once.
</p>
<ExampleViewer :component="List" :code="ListCode" />

Expand All @@ -31,16 +32,30 @@ import CustomiseCode from "./customise.vue?raw";
customised by using the <code>indicators</code> slot. The indicators
let users jump directly to a particular slide.
</p>
<ExampleViewer
:component="CustomIndicators"
:code="CustomIndicatorsCode" />
<p>Click on the slide to open the gallery mode.</p>
<ExampleViewer :component="Indicators" :code="IndicatorsCode" />

<h3 id="custom">Customise</h3>
<h3 id="autoplay">Autoplay</h3>
<p>
Using the <code>autoplay</code> prop, the carousel will run
automatically. The <code>interval</code> prop can be used to set the
cycle speed.
cycle speed. Moving focus to any of the carousel content, including
the tab elements, pauses automatic rotation, when the
<code>pause-hover</code> prop is set. If operating system
preferences have been set for reduced motion or disabling
animations, the auto-rotation is initially paused.
</p>
<div class="info custom-block">
<p class="custom-block-title">Accessibility Note:</p>
<p>
When using autoplay, users must be able to stop and start slide
rotation, which is an essential aspect of carousel accessibility
for a variety of people with disabilities. People with low
vision or a cognitive disability that affects visual processing
or reading benefit from being able to control slide rotation so
that they have enough time to explore slide content.
</p>
</div>
<ExampleViewer :component="Customise" :code="CustomiseCode" />
</div>
</template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,14 @@ function switchGallery(value): void {
<o-carousel
:autoplay="false"
:overlay="gallery"
:arrows="false"
@click="switchGallery(true)">
<o-carousel-item v-for="(item, i) in items" :key="i">
<o-carousel-item v-for="(item, i) in items" :key="i" clickable>
<div class="image">
<img :src="item.image" />
</div>
</o-carousel-item>

<template #indicators="{ active, switchTo }">
<o-carousel
:model-value="active"
Expand All @@ -81,6 +83,7 @@ function switchGallery(value): void {
</o-carousel-item>
</o-carousel>
</template>

<template #overlay>
<o-icon
v-if="gallery"
Expand Down
4 changes: 4 additions & 0 deletions packages/oruga/src/components/carousel/examples/inspector.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ const inspectData = [
class: "arrowIconNextClass",
description: "Class of next arrow element",
},
{
class: "arrowIconAutoplayClass",
description: "Class of autoplay button element",
},
{
class: "indicatorClass",
description: "Class of indicator link element",
Expand Down
1 change: 1 addition & 0 deletions packages/oruga/src/components/carousel/examples/list.vue
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,6 @@ const items = [
<img :src="item.image" />
</o-carousel-item>
</o-carousel>
<p><b>Current slide index:</b> {{ carousel }}</p>
</section>
</template>
4 changes: 3 additions & 1 deletion packages/oruga/src/components/carousel/examples/readme.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
The **Carousel** component is a slideshow for cycling through elements — images or slides of text—like a carousel.
The **Carousel** component is a slideshow for cycling through a set of elements — images or text like - a carousel, referred to as slides, by sequentially displaying a subset of one or more slides.
One slide is displayed at a time, and users can activate a next or previous slide control that hides the current slide and "rotates" the next or previous slide into view.
Tthe component implements the W3C ARIA APG [Carousel Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/carousel/).
31 changes: 18 additions & 13 deletions packages/oruga/src/components/carousel/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ export type CarouselProps = {
modelValue?: number;
/** Enable drag mode */
dragable?: boolean;
/** Timer interval for `autoplay` */
interval?: number;
/** Move item automaticalls after `interval` */
autoplay?: boolean;
/** Timer interval for `autoplay` */
interval?: number;
/** Pause autoplay on hover */
pauseHover?: boolean;
/** Repeat from the beginning after reaching the end */
Expand All @@ -21,11 +21,6 @@ export type CarouselProps = {
indicators?: boolean;
/** Place indicators inside the carousel */
indicatorInside?: boolean;
/**
* Indicator interaction mode
* @values click, hover
*/
indicatorMode?: "click" | "hover";
/** Position of the indicator - depends on used theme */
indicatorPosition?: string;
/** Style of the indicator - depends on used theme */
Expand All @@ -48,12 +43,24 @@ export type CarouselProps = {
* @values small, medium, large
*/
iconSize?: string;
/** Icon name for previous icon */
/** Icon name for previous button */
iconPrev?: string;
/** Icon name for next icon */
/** Icon name for next button */
iconNext?: string;
/** Icon name for autoplay pause button */
iconAutoplayPause?: string;
/** Icon name for autoplay resume button */
iconAutoplayResume?: string;
/** Define these props for different screen sizes */
breakpoints?: Record<number, any>;
/** Accessibility autoplay pause button aria label */
ariaAutoplayPauseLabel?: string;
/** Accessibility autoplay resume button aria label */
ariaAutoplayResumeLabel?: string;
/** Accessibility next button aria label */
ariaNextLabel?: string;
/** Accessibility previous button aria label */
ariaPreviousLabel?: string;
} & CarouselClasses;

// class props (will not be displayed in the docs)
Expand All @@ -74,14 +81,14 @@ type CarouselClasses = Partial<{
arrowIconPrevClass: ComponentClass;
/** Class of next arrow element */
arrowIconNextClass: ComponentClass;
/** Class of indicator link element */
indicatorClass: ComponentClass;
/** Class of indicators wrapper element */
indicatorsClass: ComponentClass;
/** Class of indicators wrapper element when inside */
indicatorsInsideClass: ComponentClass;
/** Class of indicators wrapper element when inside and position */
indicatorsInsidePositionClass: ComponentClass;
/** Class of indicator link element */
indicatorClass: ComponentClass;
/** Class of indicator item element */
indicatorItemClass: ComponentClass;
/** Class of indicator element when is active */
Expand All @@ -95,8 +102,6 @@ export type CarouselItemProps = {
override?: boolean;
/** Make item clickable */
clickable?: boolean;
/** Role attribute to be passed to the div wrapper for better accessibility */
ariaRole?: string;
} & CarouselItemClasses;

// class props (will not be displayed in the docs)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,136 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`OCarousel tests > render correctly 1`] = `
"<div class="o-car" data-oruga="carousel" role="region">
"<div class="o-car" data-oruga="carousel" role="region" aria-roledescription="carousel">
<div class="o-car__wrapper">
<div class="o-car__items" style="transform: translateX(0px);" tabindex="0" role="group" draggable="true" aria-roledescription="carousel">
<!--
@slot Override the pause/resume button
@binding {boolean} autoplay if autoplay is active
@binding {(): void} toggle toggle autoplay
-->
<!--v-if-->
<!--
@slot Override the arrows
@binding {boolean} has-prev has prev arrow button
@binding {boolean} has-next has next arrow button
@binding {(): void} prev switch to prev item function
@binding {(): void} next switch to next item function
--><span class="o-icon o-car__arrow__icon o-car__arrow__icon-prev" data-oruga="icon" role="button" tabindex="0" aria-label="Previous Slide" style="display: none;"><!-- custom icon component --><!-- native css icon --><i class="mdi mdi-chevron-left mdi-24px"></i></span><span class="o-icon o-car__arrow__icon o-car__arrow__icon-next" data-oruga="icon" role="button" tabindex="0" aria-label="Next Slide" style="display: none;"><!-- custom icon component --><!-- native css icon --><i class="mdi mdi-chevron-right mdi-24px"></i></span>
<div class="o-car__items" style="transform: translateX(0px);" aria-roledescription="carousel-slide" aria-atomic="false" aria-live="polite">
<!--
@slot Display carousel item
-->
</div>
</div>
<!--
@slot Override the indicators
@binding {number} active active index
@binding {(idx: number): void} switch-to switch to item function
-->
<div class="o-car__indicators" role="tablist" aria-label="Slides"></div>
<!--v-if-->
</div>"
`;

exports[`OCarousel tests > render integration correctly 1`] = `
"<div class="o-car" data-oruga="carousel" role="region" aria-roledescription="carousel">
<div class="o-car__wrapper">
<!--
@slot Override the pause/resume button
@binding {boolean} autoplay if autoplay is active
@binding {(): void} toggle toggle autoplay
-->
<!--v-if-->
<!--
@slot Override the arrows
@binding {boolean} has-prev has prev arrow button
@binding {boolean} has-next has next arrow button
@binding {(): void} prev switch to prev item function
@binding {(): void} next switch to next item function
--><span class="o-icon o-car__arrow__icon o-car__arrow__icon-prev" data-oruga="icon" role="button" tabindex="0" style="display: none;"><!-- custom icon component --><!-- native css icon --><i class="mdi mdi-chevron-left mdi-24px"></i></span><span class="o-icon o-car__arrow__icon o-car__arrow__icon-next" data-oruga="icon" role="button" tabindex="0" style="display: none;"><!-- custom icon component --><!-- native css icon --><i class="mdi mdi-chevron-right mdi-24px"></i></span>
--><span class="o-icon o-car__arrow__icon o-car__arrow__icon-prev" data-oruga="icon" role="button" tabindex="0" aria-label="Previous Slide" style="display: none;"><!-- custom icon component --><!-- native css icon --><i class="mdi mdi-chevron-left mdi-24px"></i></span><span class="o-icon o-car__arrow__icon o-car__arrow__icon-next" data-oruga="icon" role="button" tabindex="0" aria-label="Next Slide" style="display: none;"><!-- custom icon component --><!-- native css icon --><i class="mdi mdi-chevron-right mdi-24px"></i></span>
<div class="o-car__items" style="transform: translateX(0px);" aria-roledescription="carousel-slide" aria-atomic="false" aria-live="polite">
<!--
@slot Display carousel item
-->
<div id="carouselpanel-1" data-oruga="carousel-item" data-id="carousel-1" class="o-car__item o-car__item--active" style="width: 0px;" role="tabpanel" aria-labelledby="carousel-1" aria-roledescription="slide" aria-label="1 of 5" draggable="true">
<!--
@slot Default content
-->
<article class="ex-slide" style="background-color: rgb(68, 94, 0);">
<h1>Slide 1</h1>
</article>
</div>
<div id="carouselpanel-2" data-oruga="carousel-item" data-id="carousel-2" class="o-car__item" style="width: 0px;" role="tabpanel" aria-labelledby="carousel-2" aria-roledescription="slide" aria-label="2 of 5" draggable="true">
<!--
@slot Default content
-->
<article class="ex-slide" style="background-color: rgb(0, 103, 36);">
<h1>Slide 2</h1>
</article>
</div>
<div id="carouselpanel-3" data-oruga="carousel-item" data-id="carousel-3" class="o-car__item" style="width: 0px;" role="tabpanel" aria-labelledby="carousel-3" aria-roledescription="slide" aria-label="3 of 5" draggable="true">
<!--
@slot Default content
-->
<article class="ex-slide" style="background-color: rgb(182, 0, 0);">
<h1>Slide 3</h1>
</article>
</div>
<div id="carouselpanel-4" data-oruga="carousel-item" data-id="carousel-4" class="o-car__item" style="width: 0px;" role="tabpanel" aria-labelledby="carousel-4" aria-roledescription="slide" aria-label="4 of 5" draggable="true">
<!--
@slot Default content
-->
<article class="ex-slide" style="background-color: rgb(244, 195, 0);">
<h1>Slide 4</h1>
</article>
</div>
<div id="carouselpanel-5" data-oruga="carousel-item" data-id="carousel-5" class="o-car__item" style="width: 0px;" role="tabpanel" aria-labelledby="carousel-5" aria-roledescription="slide" aria-label="5 of 5" draggable="true">
<!--
@slot Default content
-->
<article class="ex-slide" style="background-color: rgb(0, 92, 152);">
<h1>Slide 5</h1>
</article>
</div>
</div>
</div>
<!--
@slot Override the indicators
@binding {number} active active index
@binding {(idx: number): void} switch-to switch to item function
@binding {number} indicator-index current indicator index
-->
<!--v-if-->
<div class="o-car__indicators o-car__indicators--inside o-car__indicators--inside--bottom" role="tablist" aria-label="Slides">
<div id="carousel-1" class="o-car__indicator" role="tab" tabindex="0" aria-label="Slide 1" aria-controls="carouselpanel-1" aria-selected="true">
<!--
@slot Override the indicator elements
@binding {index} index indicator index
--><span class="o-car__indicator__item o-car__indicator__item--dots o-car__indicator__item--active"></span>
</div>
<div id="carousel-2" class="o-car__indicator" role="tab" tabindex="-1" aria-label="Slide 2" aria-controls="carouselpanel-2" aria-selected="false">
<!--
@slot Override the indicator elements
@binding {index} index indicator index
--><span class="o-car__indicator__item o-car__indicator__item--dots"></span>
</div>
<div id="carousel-3" class="o-car__indicator" role="tab" tabindex="-1" aria-label="Slide 3" aria-controls="carouselpanel-3" aria-selected="false">
<!--
@slot Override the indicator elements
@binding {index} index indicator index
--><span class="o-car__indicator__item o-car__indicator__item--dots"></span>
</div>
<div id="carousel-4" class="o-car__indicator" role="tab" tabindex="-1" aria-label="Slide 4" aria-controls="carouselpanel-4" aria-selected="false">
<!--
@slot Override the indicator elements
@binding {index} index indicator index
--><span class="o-car__indicator__item o-car__indicator__item--dots"></span>
</div>
<div id="carousel-5" class="o-car__indicator" role="tab" tabindex="-1" aria-label="Slide 5" aria-controls="carouselpanel-5" aria-selected="false">
<!--
@slot Override the indicator elements
@binding {index} index indicator index
--><span class="o-car__indicator__item o-car__indicator__item--dots"></span>
</div>
</div>
<!--v-if-->
</div>"
`;
Loading

0 comments on commit bab6b37

Please sign in to comment.