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

[Admin] Add text_area component #5351

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
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# frozen_string_literal: true

# @api private
class SolidusAdmin::UI::Forms::Guidance::Component < SolidusAdmin::BaseComponent
def initialize(field:, form:, hint:, errors:)
@field = field
@form = form
@hint = hint
@errors = errors || @form.object&.errors || raise(ArgumentError, <<~MSG
When the form builder is not bound to a model instance, you must pass an
errors Hash (`{ field_name: [errors] }`) to the component.
MSG
)
end

def call
return "" unless needed?

tag.div(class: "mt-2") do
hint_tag + error_tag
end
end

def hint_tag
return "".html_safe unless @hint

tag.p(id: hint_id, class: "body-tiny text-gray-500 peer-disabled:text-gray-300") do
@hint
end
end

def hint_id
"#{prefix}_hint"
end

def error_tag
return "".html_safe unless errors?

tag.p(id: error_id, class: "body-tiny text-red-400") do
@errors[@field].map do |error|
tag.span(class: "block") { error.capitalize }
end.reduce(&:+)
end
end

def errors?
@errors[@field].present?
end

def error_id
"#{prefix}_error"
end

def prefix
"#{@form.object_name}_#{@field}"
end

def aria_describedby
"#{hint_id if @hint} #{error_id if errors?}"
end

def needed?
@hint || errors?
end
end
13 changes: 13 additions & 0 deletions admin/app/components/solidus_admin/ui/forms/label/component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

# @api private
class SolidusAdmin::UI::Forms::Label::Component < SolidusAdmin::BaseComponent
def initialize(field:, form:)
@field = field
@form = form
end

def call
@form.label(@field, class: "block mb-0.5 body-tiny-bold")
end
end
109 changes: 109 additions & 0 deletions admin/app/components/solidus_admin/ui/forms/text_area/component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# frozen_string_literal: true

class SolidusAdmin::UI::Forms::TextArea::Component < SolidusAdmin::BaseComponent
SIZES = {
s: %w[h-20 body-small],
m: %w[h-28 body-small],
l: %w[h-36 body-text]
}.freeze

# @param field [Symbol] the name of the field. Usually a model attribute.
# @param form [ActionView::Helpers::FormBuilder] the form builder instance.
# @param size [Symbol] the size of the field: `:s`, `:m` or `:l`.
# @param hint [String, null] helper text to display below the field.
# @param errors [Hash, nil] a Hash of errors for the field. If `nil` and the
# form is bound to a model instance, the component will automatically fetch
# the errors from the model.
# @param attributes [Hash] additional HTML attributes to add to the field.
# @raise [ArgumentError] when the form builder is not bound to a model
# instance and no `errors` Hash is passed to the component.
def initialize(
field:,
form:,
size: :m,
hint: nil,
errors: nil,
label_component: component("ui/forms/label"),
guidance_component: component("ui/forms/guidance"),
**attributes
)
@field = field
@form = form
@size = size
@hint = hint
@attributes = attributes
@errors = errors
@label_component = label_component
@guidance_component = guidance_component
end

def call
guidance = @guidance_component.new(
field: @field,
form: @form,
hint: @hint,
errors: @errors
)

tag.div(class: "mb-6") do
label_tag + field_tag(guidance) + guidance_tag(guidance)
end
end

def field_tag(guidance)
@form.text_area(
@field,
class: field_classes(guidance),
**field_aria_describedby_attribute(guidance),
**field_error_attributes(guidance),
**@attributes.except(:class)
)
end

def field_classes(guidance)
%w[
peer
block px-3 py-4 w-full
text-black
bg-white border border-gray-300 rounded-sm
hover:border-gray-500
placeholder:text-gray-400
focus:border-gray-500 focus:shadow-[0_0_0_2px_#bbb] focus-visible:outline-none
disabled:bg-gray-50 disabled:text-gray-300
] + field_size_classes + field_error_classes(guidance) + Array(@attributes[:class]).compact
end

def field_size_classes
SIZES.fetch(@size)
end

def field_aria_describedby_attribute(guidance)
return {} unless guidance.needed?

{
"aria-describedby": guidance.aria_describedby
}
end

def field_error_classes(guidance)
return [] unless guidance.errors?

%w[border-red-400 text-red-400]
end

def field_error_attributes(guidance)
return {} unless guidance.errors?

{
"aria-invalid": true
}
end

def label_tag
render @label_component.new(field: @field, form: @form)
end

def guidance_tag(guidance)
render guidance
end
end
107 changes: 43 additions & 64 deletions admin/app/components/solidus_admin/ui/forms/text_field/component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,59 +35,64 @@ class SolidusAdmin::UI::Forms::TextField::Component < SolidusAdmin::BaseComponen
# @param attributes [Hash] additional HTML attributes to add to the field.
# @raise [ArgumentError] when the form builder is not bound to a model
# instance and no `errors` Hash is passed to the component.
def initialize(field:, form:, type: :text, size: :m, hint: nil, errors: nil, **attributes)
def initialize(
field:,
form:,
type: :text,
size: :m,
hint: nil,
errors: nil,
label_component: component("ui/forms/label"),
guidance_component: component("ui/forms/guidance"),
**attributes
)
@field = field
@form = form
@type = type
@size = size
@hint = hint
@type = type
@attributes = attributes
@errors = errors || @form.object&.errors || raise(ArgumentError, <<~MSG
When the form builder is not bound to a model instance, you must pass an
errors Hash (`field_name: [errors]`) to the component.
MSG
)
@errors = errors
@label_component = label_component
@guidance_component = guidance_component
end

def call
tag.div(class: "mb-6") do
label_tag + field_tag + info_wrapper
end
end
guidance = @guidance_component.new(
field: @field,
form: @form,
hint: @hint,
errors: @errors
)

def info_wrapper
tag.div(class: "mt-2") do
hint_tag + error_tag
tag.div(class: "mb-6") do
label_tag + field_tag(guidance) + guidance_tag(guidance)
end
end

def label_tag
@form.label(@field, class: "block mb-0.5 body-tiny-bold")
end

def field_tag
def field_tag(guidance)
@form.send(
field_helper,
@field,
class: field_classes,
**field_aria_describedby_attribute,
**field_error_attributes,
class: field_classes(guidance),
**field_aria_describedby_attribute(guidance),
**field_error_attributes(guidance),
**@attributes.except(:class)
)
end

def field_classes
def field_classes(guidance)
%w[
peer
block px-3 py-1.5 w-full
text-black text-black
text-black
bg-white border border-gray-300 rounded-sm
hover:border-gray-500
placeholder:text-gray-400
focus:border-gray-500 focus:shadow-[0_0_0_2px_#bbb] focus-visible:outline-none
disabled:bg-gray-50 disabled:text-gray-300
] + field_size_classes + field_error_classes + Array(@attributes[:class]).compact
] + field_size_classes + field_error_classes(guidance) + Array(@attributes[:class]).compact
end

def field_helper
Expand All @@ -98,59 +103,33 @@ def field_size_classes
SIZES.fetch(@size)
end

def field_error_classes
return [] unless errors?
def field_aria_describedby_attribute(guidance)
return {} unless guidance.needed?

%w[border-red-400 text-red-400]
{
"aria-describedby": guidance.aria_describedby
}
end

def field_aria_describedby_attribute
return {} unless @hint || errors?
def field_error_classes(guidance)
return [] unless guidance.errors?

{
"aria-describedby": "#{hint_id if @hint} #{error_id if errors?}"
}
%w[border-red-400 text-red-400]
end

def field_error_attributes
return {} unless errors?
def field_error_attributes(guidance)
return {} unless guidance.errors?

{
"aria-invalid": true
}
end

def hint_tag
return "".html_safe unless @hint

tag.p(id: hint_id, class: "body-tiny text-gray-500 peer-disabled:text-gray-300") do
@hint
end
end

def hint_id
"#{id_prefix}_hint"
end

def error_tag
return "".html_safe unless errors?

tag.p(id: error_id, class: "body-tiny text-red-400") do
@errors[@field].map do |error|
tag.span(class: "block") { error.capitalize }
end.reduce(&:+)
end
end

def errors?
@errors[@field].present?
end

def error_id
"#{id_prefix}_error"
def label_tag
render @label_component.new(field: @field, form: @form)
end

def id_prefix
"#{@form.object_name}_#{@field}"
def guidance_tag(guidance)
render guidance
end
end
Loading