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