diff --git a/README.md b/README.md
index 1e0dde9..779ab3f 100644
--- a/README.md
+++ b/README.md
@@ -66,6 +66,7 @@ For more examples, see the demo: https://vue-ssr-carousel.netlify.app.
| `no-drag` | `false` | Disables the ability to drag the carousel.
| `show-arrows` | `false` | Whether to show back/forward arrows. See https://vue-ssr-carousel.netlify.app/ui.
| `show-dots` | `false` | Whether to show dot style pagination dots. See https://vue-ssr-carousel.netlify.app/ui.
+| `rtl` | `false` | Adjust layout for right to left sites. See https://vue-ssr-carousel.netlify.app/accessibility.
| `value` | `undefined` | Used as part of `v-model` to set the initial slide to show. See https://vue-ssr-carousel.netlify.app/events.
| `responsive` | `[]` | Adjust settings at breakpoints. See https://vue-ssr-carousel.netlify.app/responsive. Note, `loop` and `paginate-by-slide` cannot be set responsively.
diff --git a/demo/components/demos/accessibility/rtl.vue b/demo/components/demos/accessibility/rtl.vue
new file mode 100644
index 0000000..6cfd962
--- /dev/null
+++ b/demo/components/demos/accessibility/rtl.vue
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demo/content/accessibility.md b/demo/content/accessibility.md
index d9f09f7..b1d205d 100644
--- a/demo/content/accessibility.md
+++ b/demo/content/accessibility.md
@@ -43,3 +43,19 @@ By default, pages are referred to as "Page" in aria labels unless using `paginat
Story 3
```
+
+## Support RTL
+
+The `rtl` boolean props adjusts the layout and drag behavior for right-to-left sites (like when the `direction: rtl` CSS property has been set).
+
+
+
+```vue
+
+
+
+
+
+
+
+```
diff --git a/src/concerns/dimensions.coffee b/src/concerns/dimensions.coffee
index f8d3e70..3258b42 100644
--- a/src/concerns/dimensions.coffee
+++ b/src/concerns/dimensions.coffee
@@ -64,6 +64,10 @@ export default
# Check if the drag is currently out bounds
isOutOfBounds: -> @currentX > 0 or @currentX < @endX
+ # Helper for things that are triggered once dimensions are known so
+ # they can be more specific about their dependencies
+ dimensionsKnown: -> @carouselWidth and @viewportWidth
+
methods:
# Measure the component width for various calculations. Using
diff --git a/src/concerns/rtl.coffee b/src/concerns/rtl.coffee
new file mode 100644
index 0000000..2f8a4b7
--- /dev/null
+++ b/src/concerns/rtl.coffee
@@ -0,0 +1,30 @@
+###
+Code related to supporting RTL layout
+###
+export default
+
+ # Add RTL prop
+ props: rtl: Boolean
+
+ # As an easy way to support rtl, update the index to the final value
+ # when RTL is enabled. This is change is combined with reversing the order
+ # of the slides in `ssr-carousel-track`. We're testing for the
+ # dimensionsKnown value as way to ensure that the final pages count is known
+ # since it depends on knowing the width of the carousel.
+ mounted: ->
+ return unless @rtl
+ if @dimensionsKnown
+ then @setInitialRtlIndex()
+ else unwatch = @$watch 'dimensionsKnown', =>
+ @setInitialRtlIndex()
+ unwatch()
+
+ methods:
+
+ # This should only be called once. Wait a tick so we're sure that the
+ # pages value has been calculated
+ setInitialRtlIndex: ->
+ setTimeout =>
+ @index = @pages - @value - 1
+ @jumpToIndex @index
+ , 0
diff --git a/src/ssr-carousel-arrows.vue b/src/ssr-carousel-arrows.vue
index bf321a2..768037a 100644
--- a/src/ssr-carousel-arrows.vue
+++ b/src/ssr-carousel-arrows.vue
@@ -4,21 +4,27 @@
.ssr-carousel-arrows
- //- Back arrow
- button.ssr-carousel-back-button(
- :aria-label='`Previous ${pageLabel}`'
- :aria-disabled='backDisabled'
+ //- Left arrow
+ button.ssr-carousel-left-button(
+ :aria-label='rtl ? nextLabel : backLabel'
+ :aria-disabled='leftDisabled'
+ :class='rtl ? "ssr-carousel-next-button" : "ssr-carousel-back-button"'
@click='$emit("back")')
- slot(name='back' :disabled='backDisabled')
- span.ssr-carousel-back-icon
-
- //- Next arrow
- button.ssr-carousel-next-button(
- :aria-label='`Next ${pageLabel}`'
- :aria-disabled='nextDisabled'
+ slot(
+ :name='rtl ? "next" : "back"'
+ :disabled='leftDisabled')
+ span.ssr-carousel-left-icon
+
+ //- Right arrow
+ button.ssr-carousel-right-button(
+ :aria-label='rtl ? backLabel : nextLabel'
+ :aria-disabled='rightDisabled'
+ :class='rtl ? "ssr-carousel-back-button" : "ssr-carousel-next-button"'
@click='$emit("next")')
- slot(name='next' :disabled='nextDisabled')
- span.ssr-carousel-next-icon
+ slot(
+ :name='rtl ? "back" : "next"'
+ :disabled='rightDisabled')
+ span.ssr-carousel-right-icon
@@ -32,12 +38,18 @@ export default
pages: Number
shouldLoop: Boolean
pageLabel: String
+ rtl: Boolean
computed:
+ # Make the labels
+ backLabel: -> "Previous #{@pageLabel}"
+ nextLabel: -> "Next #{@pageLabel}"
+
# Determine if button should be disabled because we're at the limits
- backDisabled: -> @index == 0 unless @shouldLoop
- nextDisabled: -> @index == @pages - 1 unless @shouldLoop
+ leftDisabled: -> @index == 0 unless @shouldLoop
+ rightDisabled: -> @index == @pages - 1 unless @shouldLoop
+
@@ -48,20 +60,20 @@ export default
@import './utils'
// Vertically center buttons
-.ssr-carousel-back-button
-.ssr-carousel-next-button
+.ssr-carousel-left-button
+.ssr-carousel-right-button
v-center()
resetButton()
// Align buttons near the edges
-.ssr-carousel-back-button
+.ssr-carousel-left-button
left 2%
-.ssr-carousel-next-button
+.ssr-carousel-right-button
right 2%
// Make a default icon
-.ssr-carousel-back-icon
-.ssr-carousel-next-icon
+.ssr-carousel-left-icon
+.ssr-carousel-right-icon
// Make a circle shape
display inline-block
@@ -88,12 +100,12 @@ export default
position relative
// Make triangle icons in the buttons
-.ssr-carousel-back-icon
+.ssr-carousel-left-icon
&:before
triangle 12px, 18px, white, 'left'
left -2px // Massage center
-.ssr-carousel-next-icon
+.ssr-carousel-right-icon
&:before
triangle 12px, 18px, white, 'right'
left 2px // Massage center
diff --git a/src/ssr-carousel-dots.vue b/src/ssr-carousel-dots.vue
index 745d1ef..550471d 100644
--- a/src/ssr-carousel-dots.vue
+++ b/src/ssr-carousel-dots.vue
@@ -5,7 +5,7 @@
.ssr-carousel-dots
button.ssr-carousel-dot-button(
v-for='i in pages' :key='i'
- :aria-label='`Go to ${pageLabel} ${i}`'
+ :aria-label='makeLabel(i)'
:aria-disabled='isDisabled(i)'
@click='$emit("goto", i - 1)')
@@ -29,9 +29,15 @@ export default
boundedIndex: Number
pages: Number
pageLabel: String
+ rtl: Boolean
methods:
+ # Make the label for the dot
+ makeLabel: (index) ->
+ pageNumber = if @rtl then @pages - index + 1 else index
+ "Go to #{@pageLabel} #{pageNumber}"
+
# Check if dot index shuold be disabled
isDisabled: (index) -> @boundedIndex == index - 1
diff --git a/src/ssr-carousel-track.vue b/src/ssr-carousel-track.vue
index ff86ce6..2c4dbb7 100644
--- a/src/ssr-carousel-track.vue
+++ b/src/ssr-carousel-track.vue
@@ -11,6 +11,8 @@ export default
activeSlides: Array
leftPeekingSlideIndex: Number
rightPeekingSlideIndex: Number
+ rtl: Boolean
+ dimensionsKnown: Number
data: ->
@@ -109,9 +111,17 @@ export default
# Get the list of non-text slides, including peeking clones. This doesn't
# work as a computed function
getSlideComponents: ->
- [...(@$slots.default || []), ...(@$slots.clones || [])]
+ slides = [...(@$slots.default || []), ...(@$slots.clones || [])]
.filter (vnode) -> !vnode.text
+ # Reverses the slide if rtl and if the dimensions are known. This
+ # second condition exists to prevent the reversal from happening on SSR.
+ # Which is important because this logic is paired with setting the
+ # intial index to the last page which can't be known until the slide
+ # width is known.
+ if @rtl and @dimensionsKnown then slides = slides.reverse()
+ return slides
+
# Makes a clone of the vnode properties we'll be updating so the changes
# get rendered. Based on:
# https://github.com/vuejs/vue/issues/6052#issuecomment-313705168
@@ -161,7 +171,6 @@ export default
# Render the track and slotted slides
render: (create) ->
-
create @trackHTMLElement,
attrs: {role: "tablist" if @renderAsTablist}
class: [ 'ssr-carousel-track', { @dragging } ]
diff --git a/src/ssr-carousel.vue b/src/ssr-carousel.vue
index a8e7ca7..565f037 100644
--- a/src/ssr-carousel.vue
+++ b/src/ssr-carousel.vue
@@ -41,6 +41,8 @@
activeSlides,
leftPeekingSlideIndex,
rightPeekingSlideIndex,
+ rtl,
+ dimensionsKnown,
}`)
//- Render the slotted slides
@@ -52,7 +54,7 @@
//- Back / Next navigation
ssr-carousel-arrows(
v-if='showArrows'
- v-bind='{ index, pages, shouldLoop, pageLabel }'
+ v-bind='{ index, pages, shouldLoop, pageLabel, rtl }'
@back='back'
@next='next')
template(#back='props'): slot(name='back-arrow' v-bind='props')
@@ -61,7 +63,7 @@
//- Dots navigation
ssr-carousel-dots(
v-if='showDots'
- v-bind='{ boundedIndex, pages, pageLabel }'
+ v-bind='{ boundedIndex, pages, pageLabel, rtl }'
@goto='gotoDot')
template(#dot='props'): slot(name='dot' v-bind='props')
@@ -92,6 +94,7 @@ import looping from './concerns/looping'
import pagination from './concerns/pagination'
import peeking from './concerns/peeking'
import responsive from './concerns/responsive'
+import rtl from './concerns/rtl'
import tweening from './concerns/tweening'
import variableWidth from './concerns/variable-width'
@@ -112,6 +115,7 @@ export default
pagination
responsive
peeking # After `responsive` so prop can access `gutter` prop
+ rtl
tweening
variableWidth
]
@@ -166,6 +170,9 @@ export default
.ssr-carousel
touch-action pan-y
+ // Internal logic expects ltr layout
+ direction ltr
+
// Posiition arrows relative to this
.ssr-carousel-slides
position relative