Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Carousel component #176

Merged
merged 1 commit into from
Mar 20, 2025
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
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ GIT

GIT
remote: https://github.com/ruby-ui/ruby_ui.git
revision: cf00f6648f30031f67792440fe48c031afb581d9
revision: 67b869524eff90d2ba7b5315c45507dbad68cf88
branch: main
specs:
ruby_ui (1.0.0.beta1)
Expand Down
44 changes: 44 additions & 0 deletions app/components/ruby_ui/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 app/components/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
23 changes: 23 additions & 0 deletions app/components/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 app/components/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 app/components/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 app/components/shared/menu.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ def components
{name: "Button", path: docs_button_path},
{name: "Calendar", path: docs_calendar_path},
{name: "Card", path: docs_card_path},
{name: "Carousel", path: docs_carousel_path, badge: "New"},
# { name: "Chart", path: docs_chart_path, badge: "New" },
{name: "Checkbox", path: docs_checkbox_path},
{name: "Checkbox Group", path: docs_checkbox_group_path},
Expand Down
4 changes: 4 additions & 0 deletions app/controllers/docs_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ def card
render Views::Docs::Card.new
end

def carousel
render Views::Docs::Carousel.new
end

def calendar
render Views::Docs::Calendar.new
end
Expand Down
3 changes: 3 additions & 0 deletions app/javascript/controllers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ application.register("ruby-ui--calendar", RubyUi__CalendarController)
import RubyUi__CalendarInputController from "./ruby_ui/calendar_input_controller"
application.register("ruby-ui--calendar-input", RubyUi__CalendarInputController)

import RubyUi__CarouselController from "./ruby_ui/carousel_controller"
application.register("ruby-ui--carousel", RubyUi__CarouselController)

import RubyUi__ChartController from "./ruby_ui/chart_controller"
application.register("ruby-ui--chart", RubyUi__ChartController)

Expand Down
60 changes: 60 additions & 0 deletions app/javascript/controllers/ruby_ui/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
}
}
}
104 changes: 104 additions & 0 deletions app/views/docs/carousel.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# frozen_string_literal: true

class Views::Docs::Carousel < Views::Base
def view_template
component = "Carousel"
div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do
render Docs::Header.new(title: "Carousel", description: "A carousel with motion and swipe built using Embla.")

Heading(level: 2) { "Usage" }

render Docs::VisualCodeExample.new(title: "Example", context: self) do
<<~RUBY
Carousel(options: {loop:false}, class: "w-full max-w-xs") do
CarouselContent do
5.times do |index|
CarouselItem do
div(class: "p-1") do
Card do
CardContent(class: "flex aspect-square items-center justify-center p-6") do
span(class: "text-4xl font-semibold") { index + 1 }
end
end
end
end
end
end
CarouselPrevious()
CarouselNext()
end
RUBY
end

render Docs::VisualCodeExample.new(title: "Sizes", context: self) do
<<~RUBY
Carousel(class: "w-full max-w-sm") do
CarouselContent do
5.times do |index|
CarouselItem(class: "md:basis-1/2 lg:basis-1/3") do
div(class: "p-1") do
Card do
CardContent(class: "flex aspect-square items-center justify-center p-6") do
span(class: "text-3xl font-semibold") { index + 1 }
end
end
end
end
end
end
CarouselPrevious()
CarouselNext()
end
RUBY
end

render Docs::VisualCodeExample.new(title: "Spacing", context: self) do
<<~RUBY
Carousel(class: "w-full max-w-sm") do
CarouselContent(class: "-ml-1") do
5.times do |index|
CarouselItem(class: "pl-1 md:basis-1/2 lg:basis-1/3") do
div(class: "p-1") do
Card do
CardContent(class: "flex aspect-square items-center justify-center p-6") do
span(class: "text-2xl font-semibold") { index + 1 }
end
end
end
end
end
end
CarouselPrevious()
CarouselNext()
end
RUBY
end

render Docs::VisualCodeExample.new(title: "Orientation", context: self) do
<<~RUBY
Carousel(orientation: :vertical, options: {align: "start"}, class: "w-full max-w-xs") do
CarouselContent(class: "-mt-1 h-[200px]") do
5.times do |index|
CarouselItem(class: "pt-1 md:basis-1/2") do
div(class: "p-1") do
Card do
CardContent(class: "flex items-center justify-center p-6") do
span(class: "text-3xl font-semibold") { index + 1 }
end
end
end
end
end
end
CarouselPrevious()
CarouselNext()
end
RUBY
end

render Components::ComponentSetup::Tabs.new(component_name: component)

render Docs::ComponentsTable.new(component_files(component))
end
end
end
Loading
Loading