Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/generators/ruby_ui/dependencies.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ calendar:
js_packages:
- "mustache"

carousel:
js_packages:
- "embla-carousel"

chart:
js_packages:
- "chart.js"
Expand Down
44 changes: 44 additions & 0 deletions lib/ruby_ui/carousel/carousel.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

module RubyUI
class Carousel < Base
def initialize(orientation: :horizontal, options: {}, **user_attrs)
@orientation = orientation
@options = options

super(**user_attrs)
end

def view_template(&)
div(**attrs, &)
end

private

def default_attrs
{
class: ["relative group", orientation_classes],
role: "region",
aria_roledescription: "carousel",
data: {
controller: "ruby-ui--carousel",
ruby_ui__carousel_options_value: default_options.merge(@options).to_json,
action: %w[
keydown.right->ruby-ui--carousel#scrollNext:prevent
keydown.left->ruby-ui--carousel#scrollPrev:prevent
]
}
}
end

def default_options
{
axis: (@orientation == :horizontal) ? "x" : "y"
}
end

def orientation_classes
(@orientation == :horizontal) ? "is-horizontal" : "is-vertical"
end
end
end
23 changes: 23 additions & 0 deletions lib/ruby_ui/carousel/carousel_content.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

module RubyUI
class CarouselContent < Base
def view_template(&)
div(class: "overflow-hidden", data: {ruby_ui__carousel_target: "viewport"}) do
div(**attrs, &)
end
end

private

def default_attrs
{
class: [
"flex",
"group-[.is-horizontal]:-ml-4",
"group-[.is-vertical]:-mt-4 group-[.is-vertical]:flex-col"
]
}
end
end
end
60 changes: 60 additions & 0 deletions lib/ruby_ui/carousel/carousel_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Controller } from "@hotwired/stimulus";
import EmblaCarousel from 'embla-carousel'

const DEFAULT_OPTIONS = {
loop: true
}

export default class extends Controller {
static values = {
options: {
type: Object,
default: {},
}
}
static targets = ["viewport", "nextButton", "prevButton"]

connect() {
this.initCarousel(this.#mergedOptions)
}

disconnect() {
this.destroyCarousel()
}

initCarousel(options, plugins = []) {
this.carousel = EmblaCarousel(this.viewportTarget, options, plugins)

this.carousel.on("init", this.#updateControls.bind(this))
this.carousel.on("reInit", this.#updateControls.bind(this))
this.carousel.on("select", this.#updateControls.bind(this))
}

destroyCarousel() {
this.carousel.destroy()
}

scrollNext() {
this.carousel.scrollNext()
}

scrollPrev() {
this.carousel.scrollPrev()
}

#updateControls() {
this.#toggleButtonsDisabledState(this.nextButtonTargets, !this.carousel.canScrollNext())
this.#toggleButtonsDisabledState(this.prevButtonTargets, !this.carousel.canScrollPrev())
}

#toggleButtonsDisabledState(buttons, isDisabled) {
buttons.forEach((button) => button.disabled = isDisabled)
}

get #mergedOptions() {
return {
...DEFAULT_OPTIONS,
...this.optionsValue
}
}
}
23 changes: 23 additions & 0 deletions lib/ruby_ui/carousel/carousel_item.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

module RubyUI
class CarouselItem < Base
def view_template(&)
div(**attrs, &)
end

private

def default_attrs
{
role: "group",
aria_roledescription: "slide",
class: [
"min-w-0 shrink-0 grow-0 basis-full",
"group-[.is-horizontal]:pl-4",
"group-[.is-vertical]:pt-4"
]
}
end
end
end
48 changes: 48 additions & 0 deletions lib/ruby_ui/carousel/carousel_next.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# frozen_string_literal: true

module RubyUI
class CarouselNext < Base
def view_template(&)
Button(**attrs) do
icon
end
end

private

def default_attrs
{
variant: :outline,
icon: true,
class: [
"absolute h-8 w-8 rounded-full",
"group-[.is-horizontal]:-right-12 group-[.is-horizontal]:top-1/2 group-[.is-horizontal]:-translate-y-1/2",
"group-[.is-vertical]:-bottom-12 group-[.is-vertical]:left-1/2 group-[.is-vertical]:-translate-x-1/2 group-[.is-vertical]:rotate-90"
],
disabled: true,
data: {
action: "click->ruby-ui--carousel#scrollNext",
ruby_ui__carousel_target: "nextButton"
}
}
end

def icon
svg(
width: "24",
height: "24",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
stroke_width: "2",
stroke_linecap: "round",
stroke_linejoin: "round",
xmlns: "http://www.w3.org/2000/svg",
class: "w-4 h-4"
) do |s|
s.path(d: "M5 12h14")
s.path(d: "m12 5 7 7-7 7")
end
end
end
end
49 changes: 49 additions & 0 deletions lib/ruby_ui/carousel/carousel_previous.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

module RubyUI
class CarouselPrevious < Base
def view_template(&)
Button(**attrs) do
icon
span(class: "sr-only") { "Next slide" }
end
end

private

def default_attrs
{
variant: :outline,
icon: true,
class: [
"absolute h-8 w-8 rounded-full",
"group-[.is-horizontal]:-left-12 group-[.is-horizontal]:top-1/2 group-[.is-horizontal]:-translate-y-1/2",
"group-[.is-vertical]:-top-12 group-[.is-vertical]:left-1/2 group-[.is-vertical]:-translate-x-1/2 group-[.is-vertical]:rotate-90"
],
disabled: true,
data: {
action: "click->ruby-ui--carousel#scrollPrev",
ruby_ui__carousel_target: "prevButton"
}
}
end

def icon
svg(
width: "24",
height: "24",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
stroke_width: "2",
stroke_linecap: "round",
stroke_linejoin: "round",
xmlns: "http://www.w3.org/2000/svg",
class: "w-4 h-4"
) do |s|
s.path(d: "m12 19-7-7 7-7")
s.path(d: "M19 12H5")
end
end
end
end
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@hotwired/stimulus": "^3.2.2",
"chart.js": "^4.4.1",
"date-fns": "^2.30.0",
"embla-carousel": "8.5.2",
"fuse.js": "^7.0.0",
"maska": "^3.0.3",
"motion": "^10.16.4",
Expand Down
49 changes: 49 additions & 0 deletions test/ruby_ui/carousel_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# frozen_string_literal: true

require "test_helper"

class RubyUI::CarouselTest < ComponentTest
def test_render_with_all_items
output = phlex do
RubyUI.Carousel do
RubyUI.CarouselContent do
RubyUI.CarouselItem { "Item" }
end
RubyUI.CarouselPrevious()
RubyUI.CarouselNext()
end
end

assert_match(/Item/, output)
assert_match(/button/, output)
assert_match(/ is-horizontal/, output)
end

def test_render_with_horizontal_orientation
output = phlex do
RubyUI.Carousel(orientation: :horizontal) do
RubyUI.CarouselContent() do
RubyUI.CarouselItem() { "Item" }
end
RubyUI.CarouselPrevious()
RubyUI.CarouselNext()
end
end

assert_match(/ is-horizontal/, output)
end

def test_render_with_vertical_orientation
output = phlex do
RubyUI.Carousel(orientation: :vertical) do
RubyUI.CarouselContent() do
RubyUI.CarouselItem() { "Item" }
end
RubyUI.CarouselPrevious()
RubyUI.CarouselNext()
end
end

assert_match(/ is-vertical/, output)
end
end
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ date-fns@^2.30.0:
dependencies:
"@babel/runtime" "^7.21.0"

embla-carousel@8.5.2:
version "8.5.2"
resolved "https://registry.yarnpkg.com/embla-carousel/-/embla-carousel-8.5.2.tgz#95eb936d14a1b9a67b9207a0fde1f25259a5d692"
integrity sha512-xQ9oVLrun/eCG/7ru3R+I5bJ7shsD8fFwLEY7yPe27/+fDHCNj0OT5EoG5ZbFyOxOcG6yTwW8oTz/dWyFnyGpg==

fuse.js@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-7.0.0.tgz#6573c9fcd4c8268e403b4fc7d7131ffcf99a9eb2"
Expand Down