-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
As with the other form components, it's design to be used along a Rails' form builder instance. We need to use Javascript to style the select box differently when the prompt option is the one selected. Ref. #5329
- Loading branch information
1 parent
9a7d0de
commit 839dfb3
Showing
6 changed files
with
345 additions
and
0 deletions.
There are no files selected for viewing
33 changes: 33 additions & 0 deletions
33
admin/app/components/solidus_admin/ui/forms/select/component.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { Controller } from '@hotwired/stimulus' | ||
|
||
export default class extends Controller { | ||
static targets = ['select', 'arrow'] | ||
static classes = ['regular', 'prompt', 'arrowPrompt'] | ||
|
||
connect () { | ||
this.addClassToOptions() | ||
this.refreshSelectClass() | ||
} | ||
|
||
// Add class to all the options to avoid inheriting the select's styles | ||
addClassToOptions () { | ||
this.selectTarget.querySelectorAll('option').forEach((option) => { | ||
if (option.value == '') { | ||
option.classList.add(this.promptClass) | ||
} else { | ||
option.classList.add(this.regularClass) | ||
} | ||
}) | ||
} | ||
|
||
// Make the select look like a placeholder when the prompt is selected | ||
refreshSelectClass () { | ||
if (this.selectTarget.options[this.selectTarget.selectedIndex].value == '') { | ||
this.selectTarget.classList.add(this.promptClass) | ||
this.arrowTarget.classList.add(this.arrowPromptClass) | ||
} else { | ||
this.selectTarget.classList.remove(this.promptClass) | ||
this.arrowTarget.classList.remove(this.arrowPromptClass) | ||
} | ||
} | ||
} |
161 changes: 161 additions & 0 deletions
161
admin/app/components/solidus_admin/ui/forms/select/component.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
# frozen_string_literal: true | ||
|
||
class SolidusAdmin::UI::Forms::Select::Component < SolidusAdmin::BaseComponent | ||
SIZES = { | ||
s: { | ||
select: %w[leading-4 body-small], | ||
arrow: %w[w-4 h-4] | ||
}, | ||
m: { | ||
select: %w[leading-6 body-small], | ||
arrow: %w[w-5 h-5] | ||
}, | ||
l: { | ||
select: %w[leading-9 body-text], | ||
arrow: %w[w-6 h-6] | ||
} | ||
}.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 choices [Array] an array of choices for the select box. All the | ||
# formats valid for Rails' `select` helper are supported. | ||
# @param hint [String, null] helper text to display below the select box. | ||
# @param errors [Hash, nil] a Hash of errors for the field. If `nil` and the | ||
# builder is bound to a model instance, the component will automatically fetch | ||
# the errors from the model. | ||
# @param options [Hash] additional options to pass to Rails' `select` helper. | ||
# @param attributes [Hash] additional HTML attributes to add to the select box. | ||
# @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, | ||
choices: [], | ||
hint: nil, | ||
errors: nil, | ||
label_component: component("ui/forms/label"), | ||
guidance_component: component("ui/forms/guidance"), | ||
options: {}, | ||
attributes: {} | ||
) | ||
@field = field | ||
@form = form | ||
@size = size | ||
@choices = choices | ||
@hint = hint | ||
@options = options | ||
@attributes = HashWithIndifferentAccess.new(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, | ||
disabled: @attributes[:disabled] | ||
) | ||
|
||
tag.div(class: "mb-6") do | ||
label_tag + field_wrapper_tag(guidance) + guidance_tag(guidance) | ||
end | ||
end | ||
|
||
def field_wrapper_tag(guidance) | ||
tag.div( | ||
class: "relative", | ||
"data-controller" => stimulus_id, | ||
"data-#{stimulus_id}-regular-class" => "text-black", | ||
"data-#{stimulus_id}-prompt-class" => "text-gray-400", | ||
"data-#{stimulus_id}-arrow-prompt-class" => "!fill-gray-500" | ||
) do | ||
field_tag(guidance) + arrow_tag(guidance) | ||
end | ||
end | ||
|
||
def field_tag(guidance) | ||
@form.select( | ||
@field, | ||
@choices, | ||
@options, | ||
class: field_classes(guidance), | ||
**field_aria_describedby_attribute(guidance), | ||
**field_error_attributes(guidance), | ||
**@attributes.except(:class).merge( | ||
"data-target" => "#{stimulus_id}.select", | ||
"data-action" => "#{stimulus_id}#refreshSelectClass" | ||
) | ||
) | ||
end | ||
|
||
def field_classes(guidance) | ||
%w[ | ||
block px-3 py-1.5 w-full | ||
appearance-none | ||
text-black | ||
bg-white border border-gray-300 rounded-sm | ||
hover:border-gray-500 | ||
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)[:select] | ||
end | ||
|
||
def field_error_classes(guidance) | ||
return [] unless guidance.errors? | ||
|
||
%w[border-red-400 text-red-400] | ||
end | ||
|
||
def field_aria_describedby_attribute(guidance) | ||
return {} unless guidance.needed? | ||
|
||
{ | ||
"aria-describedby": guidance.aria_describedby | ||
} | ||
end | ||
|
||
def field_error_attributes(guidance) | ||
return {} unless guidance.errors? | ||
|
||
{ | ||
"aria-invalid": true | ||
} | ||
end | ||
|
||
def arrow_tag(guidance) | ||
icon_tag( | ||
"arrow-down-s-fill", | ||
class: SIZES.fetch(@size)[:arrow] + [arrow_color_class(guidance)] + | ||
%w[absolute right-3 top-1/2 translate-y-[-50%] pointer-events-none], | ||
"data-target" => "#{stimulus_id}.arrow" | ||
) | ||
end | ||
|
||
def arrow_color_class(guidance) | ||
if @attributes[:disabled] | ||
"fill-gray-500" | ||
elsif guidance.errors? | ||
"fill-red-400" | ||
else | ||
"fill-gray-700" | ||
end | ||
end | ||
|
||
def label_tag | ||
render @label_component.new(field: @field, form: @form) | ||
end | ||
|
||
def guidance_tag(guidance) | ||
render guidance | ||
end | ||
end |
85 changes: 85 additions & 0 deletions
85
admin/spec/components/previews/solidus_admin/ui/forms/select/component_preview.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
# frozen_string_literal: true | ||
|
||
# @component "ui/forms/select" | ||
class SolidusAdmin::UI::Forms::Select::ComponentPreview < ViewComponent::Preview | ||
include SolidusAdmin::Preview | ||
|
||
# The select component is used to render a select box in a form. | ||
# | ||
# See the [`ui/forms/text_field`](../text_field) component for usage | ||
# instructions. | ||
def overview | ||
choices = [ | ||
["Option 1", "option_1"], | ||
["Option 2", "option_2"], | ||
["Option 3", "option_3"] | ||
] | ||
hint = "Select one of the options" | ||
render_with_template( | ||
locals: { | ||
sizes: current_component::SIZES.keys, | ||
choices: choices, | ||
variants: { | ||
"with_prompt" => { | ||
hint: nil, errors: {}, options: { prompt: "Select" }, attributes: {} | ||
}, | ||
"selected" => { | ||
hint: nil, errors: {}, options: {}, attributes: {} | ||
}, | ||
"with_hint" => { | ||
hint: hint, errors: {}, options: {}, attributes: {} | ||
}, | ||
"with_prompt_and_error" => { | ||
hint: nil, errors: { "with_prompt_and_error" => ["can't be blank"] }, options: { prompt: "Select" }, attributes: {} | ||
}, | ||
"selected_with_error" => { | ||
hint: nil, errors: { "selected_with_error" => ["is invalid"] }, options: {}, attributes: {} | ||
}, | ||
"with_hint_and_error" => { | ||
hint: hint, errors: { "with_hint_and_error" => ["is invalid"] }, options: {}, attributes: {} | ||
}, | ||
"with_prompt_disabled" => { | ||
hint: nil, errors: {}, options: { prompt: "Select" }, attributes: { disabled: true } | ||
}, | ||
"selected_disabled" => { | ||
hint: nil, errors: {}, options: {}, attributes: { disabled: true } | ||
}, | ||
"with_hint_disabled" => { | ||
hint: hint, errors: {}, options: {}, attributes: { disabled: true } | ||
} | ||
} | ||
} | ||
) | ||
end | ||
|
||
# @param size select { choices: [s, m, l] } | ||
# @param choices text "Separate multiple choices with a comma" | ||
# @param label text | ||
# @param selected text | ||
# @param hint text | ||
# @param errors text "Separate multiple errors with a comma" | ||
# @param prompt text | ||
# @param disabled toggle | ||
def playground( | ||
size: :m, | ||
choices: "Option 1, Option 2, Option 3", | ||
label: "Choose:", | ||
selected: "Option 1", | ||
hint: nil, errors: "", | ||
prompt: "Select", | ||
disabled: false | ||
) | ||
render_with_template( | ||
locals: { | ||
size: size.to_sym, | ||
choices: choices.split(",").map(&:strip).map { [_1, _1.parameterize] }, | ||
field: label, | ||
selected: selected&.parameterize, | ||
hint: hint, | ||
errors: { label.dasherize => (errors.blank? ? [] : errors.split(",").map(&:strip)) }, | ||
prompt: prompt, | ||
disabled: disabled | ||
} | ||
) | ||
end | ||
end |
34 changes: 34 additions & 0 deletions
34
...pec/components/previews/solidus_admin/ui/forms/select/component_preview/overview.html.erb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<%= form_with(url: "#", scope: :overview, method: :get, class: "w-full") do |form| %> | ||
<table> | ||
<thead> | ||
<tr> | ||
<% sizes.each do |size| %> | ||
<td class="px-3 py-1 text-gray-500 text-center body-text"><%= size.to_s.humanize %></td> | ||
<% end %> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
<% | ||
variants.each_pair do |name, definition| %> | ||
<tr> | ||
<% sizes.each do |size| %> | ||
<td class="px-3 py-1"> | ||
<%= | ||
render current_component.new( | ||
form: form, | ||
field: name, | ||
choices: choices, | ||
size: size, | ||
errors: definition[:errors], | ||
hint: definition[:hint], | ||
options: definition[:options], | ||
attributes: definition[:attributes] | ||
) | ||
%> | ||
</td> | ||
<% end %> | ||
</tr> | ||
<% end %> | ||
</tbody> | ||
</table> | ||
<% end %> |
19 changes: 19 additions & 0 deletions
19
...c/components/previews/solidus_admin/ui/forms/select/component_preview/playground.html.erb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<%= form_with(url: "#", scope: :playground, method: :get, class: "w-60") do |form| %> | ||
<%= | ||
render current_component.new( | ||
form: form, | ||
size: size, | ||
choices: choices, | ||
field: field, | ||
hint: hint, | ||
errors: errors, | ||
options: { | ||
prompt: prompt, | ||
selected: selected | ||
}, | ||
attributes: { | ||
disabled: disabled | ||
} | ||
) | ||
%> | ||
<% end %> |
13 changes: 13 additions & 0 deletions
13
admin/spec/components/solidus_admin/ui/forms/select/component_spec.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
# frozen_string_literal: true | ||
|
||
require "spec_helper" | ||
|
||
RSpec.describe SolidusAdmin::UI::Forms::Select::Component, type: :component do | ||
it "renders the overview preview" do | ||
render_preview(:overview) | ||
end | ||
|
||
it "renders the playground preview" do | ||
render_preview(:playground) | ||
end | ||
end |