Skip to content

Commit

Permalink
Add a low-level ui/input component
Browse files Browse the repository at this point in the history
This will represent all input shaped elements like `input`, `textarea`
and `select`.
  • Loading branch information
elia committed Sep 19, 2023
1 parent 91f7f8c commit 979cf76
Show file tree
Hide file tree
Showing 8 changed files with 250 additions and 0 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions admin/app/components/solidus_admin/ui/forms/input/component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Controller } from '@hotwired/stimulus'

export default class extends Controller {
static values = {
customValidity: String,
}

connect() {
if (this.customValidityValue)
this.element.setCustomValidity(this.customValidityValue)
}

clearCustomValidity() {
this.element.setCustomValidity('')
}
}
99 changes: 99 additions & 0 deletions admin/app/components/solidus_admin/ui/forms/input/component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# frozen_string_literal: true

class SolidusAdmin::UI::Forms::Input::Component < SolidusAdmin::BaseComponent
SIZES = {
s: "form-control-sm px-3 py-1.5 body-small",
m: "form-control-md px-3 py-1.5 body-small",
l: "form-control-lg px-3 py-1.5 body-text"
}.freeze

HEIGHTS = {
s: "h-7",
m: "h-9",
l: "h-12"
}.freeze

MULTILINE_HEIGHTS = {
s: %w[min-h-[84px]],
m: %w[min-h-[108px]],
l: %w[min-h-[144px]],
}.freeze

TYPES = Set.new(%i[
text
password
number
email
tel
url
search
color
date
datetime-local
month
week
time
]).freeze

def initialize(tag: :input, size: :m, error: nil, **attributes)
specialized_classes = []

case tag
when :input
specialized_classes << "form-input"
specialized_classes << HEIGHTS[size]
if attributes[:type] && !TYPES.include?(attributes[:type])
raise ArgumentError, "unsupported type attribute: #{attributes[:type]}"
end
when :textarea
specialized_classes << "form-textarea"
specialized_classes << MULTILINE_HEIGHTS[size]
when :select
if attributes[:multiple]
specialized_classes << "form-multiselect"
specialized_classes << MULTILINE_HEIGHTS[size]
else
specialized_classes << "form-select"
specialized_classes << "bg-arrow-down-s-fill-gray-700 invalid:bg-arrow-down-s-fill-red-400 aria-invalid:bg-arrow-down-s-fill-red-400"
specialized_classes << HEIGHTS[size]
end
end

attributes[:class] = [
%w[
w-full
text-black bg-white border border-gray-300 rounded-sm placeholder:text-gray-400
hover:border-gray-500
focus:ring focus:ring-gray-300 focus:ring-0.5 focus:bg-white focus:ring-offset-0 [&:focus-visible]:outline-none
disabled:bg-gray-50 disabled:text-gray-500 disabled:placeholder:text-gray-300 disabled:cursor-not-allowed
invalid:border-red-400 invalid:hover:border-red-400 invalid:text-red-400
aria-invalid:border-red-400 aria-invalid:hover:border-red-400 aria-invalid:text-red-400
],
SIZES[size],
specialized_classes,
attributes[:class],
].compact.join(" ")

@tag = tag
@size = size
@error = error
@attributes = attributes

raise ArgumentError, "unsupported tag: #{tag}" unless %i[input textarea select].include?(@tag)
end

def call
if @tag == :select && @attributes[:choices]
with_content options_for_select(@attributes.delete(:choices), @attributes.delete(:value))
end

tag.public_send(
@tag,
content,
"data-controller": stimulus_id,
"data-#{stimulus_id}-custom-validity-value": @error.presence,
"data-action": "#{stimulus_id}#clearCustomValidity",
**@attributes
)
end
end
2 changes: 2 additions & 0 deletions admin/config/solidus_admin/tailwind.config.js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ module.exports = {
},
backgroundImage: {
'arrow-right-up-line': "url('solidus_admin/arrow_right_up_line.svg')",
'arrow-down-s-fill-gray-700': "url('solidus_admin/arrow_down_s_fill_gray_700.svg')",
'arrow-down-s-fill-red-400': "url('solidus_admin/arrow_down_s_fill_red_400.svg')",
},
boxShadow: {
sm: '0px 1px 2px 0px rgba(0, 0, 0, 0.04)',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# frozen_string_literal: true

# @component "ui/forms/input"
class SolidusAdmin::UI::Forms::Input::ComponentPreview < ViewComponent::Preview
include SolidusAdmin::Preview

def overview
render_with_template
end

# @param error toggle
# @param size select { choices: [s, m, l] }
# @param value text
# @param type select :input_types
def input_playground(error: false, size: "m", value: "value", type: "text")
render component("ui/forms/input").new(
tag: :input,
type: type.to_sym,
error: error ? "There is an error" : nil,
size: size.to_sym,
value: value,
)
end

# @param error toggle
# @param size select { choices: [s, m, l] }
# @param multiple toggle
# @param rows number
# @param options number
# @param include_blank toggle
def select_playground(error: false, include_blank: true, options: 3, rows: 1, size: "m", multiple: false)
options = (1..options).map { |i| ["Option #{i}", i] }
options.unshift(["", ""]) if include_blank
options.map! { tag.option(_1, value: _2) }

render component("ui/forms/input").new(
tag: :select,
"size" => rows > 1 ? rows : nil,
error: error ? "There is an error" : nil,
size: size.to_sym,
multiple: multiple,
).with_content(options.reduce(:+))
end

# @param error toggle
# @param size select { choices: [s, m, l] }
# @param content textarea
def textarea_playground(error: false, size: "m", content: "value")
render component("ui/forms/input").new(
tag: :textarea,
size: size.to_sym,
error: error ? "There is an error" : nil,
).with_content(content)
end

private

def input_types
current_component::TYPES.to_a
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<section>
<h3>Input</h3>

<div class="mb-8">
<h6 class="text-gray-500 mb-3 mt-0">Default</h6>
<%= render current_component.new(placeholder: "Placeholder") %>
</div>

<div class="mb-8">
<h6 class="text-gray-500 mb-3 mt-0">Filled</h6>
<%= render current_component.new(value: "My value") %>
</div>

<div class="mb-8">
<h6 class="text-gray-500 mb-3 mt-0">Error</h6>
<%= render current_component.new(value: "Bad value", error: "The value is wrong!") %>
</div>

<div class="mb-8">
<h6 class="text-gray-500 mb-3 mt-0">Disabled</h6>
<%= render current_component.new(placeholder: "Placeholder", disabled: true) %>
</div>

<div class="mb-8">
<h6 class="text-gray-500 mb-3 mt-0">Disabled filled</h6>
<%= render current_component.new(value: "My value", disabled: true) %>
</div>
</section>
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe SolidusAdmin::UI::Forms::Input::Component, type: :component do
it "renders the overview preview" do
render_preview(:overview)
render_preview(:input_playground)
render_preview(:select_playground)
render_preview(:textarea_playground)
end

it "only accepts certain 'type' attributes for the input" do
expect {
render_inline(described_class.new(type: :button))
}.to raise_error(ArgumentError, /unsupported type attribute: button/)
end

describe "with `tag: input`" do
it "renders a text input" do
render_inline(described_class.new(type: :text, name: "name", value: "value"))

expect(page).to have_css("input[type='text'][name='name'][value='value']")
end

it "renders a password input" do
render_inline(described_class.new(type: :password, name: "name", value: "value"))

expect(page).to have_css("input[type='password'][name='name'][value='value']")
end

it "renders a number input" do
render_inline(described_class.new(type: :number, name: "name", value: "value"))

expect(page).to have_css("input[type='number'][name='name'][value='value']")
end
end
end

0 comments on commit 979cf76

Please sign in to comment.