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
39 changes: 39 additions & 0 deletions lib/ruby_ui/native_select/native_select.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

module RubyUI
class NativeSelect < Base
def initialize(size: :default, **attrs)
@size = size
super(**attrs)
end

def view_template(&block)
div(
class: "group/native-select relative w-fit has-[select:disabled]:opacity-50"
) do
select(**attrs, &block)
render RubyUI::NativeSelectIcon.new
end
end

private

def default_attrs
{
data: {
ruby_ui__form_field_target: "input",
action: "change->ruby-ui--form-field#onChange invalid->ruby-ui--form-field#onInvalid"
},
class: [
"border-border bg-transparent text-sm w-full min-w-0 appearance-none rounded-md border py-1 pr-8 pl-2.5 shadow-xs transition-[color,box-shadow] outline-none select-none ring-0 ring-ring/0",
"placeholder:text-muted-foreground",
"selection:bg-primary selection:text-primary-foreground",
"focus-visible:outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-2",
"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
"aria-invalid:ring-destructive/20 aria-invalid:border-destructive aria-invalid:ring-2",
(@size == :sm) ? "h-7 rounded-md py-0.5" : "h-9"
]
}
end
end
end
83 changes: 83 additions & 0 deletions lib/ruby_ui/native_select/native_select_docs.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# frozen_string_literal: true

class Views::Docs::NativeSelect < Views::Base
def view_template
component = "NativeSelect"

div(class: "max-w-2xl mx-auto w-full py-10 space-y-10") do
render Docs::Header.new(title: "Native Select", description: "A styled native HTML select element with consistent design system integration.")

Heading(level: 2) { "Usage" }

render Docs::VisualCodeExample.new(title: "Default", context: self) do
<<~RUBY
div(class: "grid w-full max-w-sm items-center gap-1.5") do
NativeSelect do
NativeSelectOption(value: "") { "Select a fruit" }
NativeSelectOption(value: "apple") { "Apple" }
NativeSelectOption(value: "banana") { "Banana" }
NativeSelectOption(value: "blueberry") { "Blueberry" }
NativeSelectOption(value: "pineapple") { "Pineapple" }
end
end
RUBY
end

render Docs::VisualCodeExample.new(title: "Groups", description: "Use NativeSelectGroup to organize options into categories.", context: self) do
<<~RUBY
div(class: "grid w-full max-w-sm items-center gap-1.5") do
NativeSelect do
NativeSelectOption(value: "") { "Select a department" }
NativeSelectGroup(label: "Engineering") do
NativeSelectOption(value: "frontend") { "Frontend" }
NativeSelectOption(value: "backend") { "Backend" }
NativeSelectOption(value: "devops") { "DevOps" }
end
NativeSelectGroup(label: "Sales") do
NativeSelectOption(value: "account_executive") { "Account Executive" }
NativeSelectOption(value: "sales_development") { "Sales Development" }
end
end
end
RUBY
end

render Docs::VisualCodeExample.new(title: "Disabled", description: "Add the disabled attribute to the NativeSelect component to disable the select.", context: self) do
<<~RUBY
div(class: "grid w-full max-w-sm items-center gap-1.5") do
NativeSelect(disabled: true) do
NativeSelectOption(value: "") { "Select a fruit" }
NativeSelectOption(value: "apple") { "Apple" }
NativeSelectOption(value: "banana") { "Banana" }
NativeSelectOption(value: "blueberry") { "Blueberry" }
end
end
RUBY
end

render Docs::VisualCodeExample.new(title: "Invalid", description: "Use aria-invalid to show validation errors.", context: self) do
<<~RUBY
div(class: "grid w-full max-w-sm items-center gap-1.5") do
NativeSelect(aria: {invalid: "true"}) do
NativeSelectOption(value: "") { "Select a fruit" }
NativeSelectOption(value: "apple") { "Apple" }
NativeSelectOption(value: "banana") { "Banana" }
NativeSelectOption(value: "blueberry") { "Blueberry" }
end
end
RUBY
end

Heading(level: 2) { "Native Select vs Select" }

div(class: "space-y-2 text-sm text-muted-foreground") do
p { "NativeSelect: Choose for native browser behavior, superior performance, or mobile-optimized dropdowns." }
p { "Select: Choose for custom styling, animations, or complex interactions." }
end

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

render Docs::ComponentsTable.new(component_files(component))
end
end
end
15 changes: 15 additions & 0 deletions lib/ruby_ui/native_select/native_select_group.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module RubyUI
class NativeSelectGroup < Base
def view_template(&)
optgroup(**attrs, &)
end

private

def default_attrs
{}
end
end
end
39 changes: 39 additions & 0 deletions lib/ruby_ui/native_select/native_select_icon.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

module RubyUI
class NativeSelectIcon < Base
def view_template(&block)
span(**attrs) do
if block
block.call
else
icon
end
end
end

private

def icon
svg(
xmlns: "http://www.w3.org/2000/svg",
viewbox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
stroke_width: "2",
stroke_linecap: "round",
stroke_linejoin: "round",
class: "size-4",
aria_hidden: "true"
) do |s|
s.path(d: "m6 9 6 6 6-6")
end
end

def default_attrs
{
class: "text-muted-foreground pointer-events-none absolute top-1/2 right-2.5 -translate-y-1/2 select-none"
}
end
end
end
15 changes: 15 additions & 0 deletions lib/ruby_ui/native_select/native_select_option.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module RubyUI
class NativeSelectOption < Base
def view_template(&)
option(**attrs, &)
end

private

def default_attrs
{}
end
end
end
23 changes: 23 additions & 0 deletions test/ruby_ui/native_select_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

require "test_helper"

class RubyUI::NativeSelectTest < ComponentTest
def test_render_with_all_items
output = phlex do
RubyUI.NativeSelect(name: "department") do
RubyUI.NativeSelectOption(value: "") { "Select a department" }
RubyUI.NativeSelectGroup(label: "Engineering") do
RubyUI.NativeSelectOption(value: "frontend") { "Frontend" }
RubyUI.NativeSelectOption(value: "backend") { "Backend" }
end
RubyUI.NativeSelectGroup(label: "Sales") do
RubyUI.NativeSelectOption(value: "account_executive") { "Account Executive" }
end
end
end

assert_match(/Frontend/, output)
assert_match('name="department"', output)
end
end