diff --git a/admin/app/assets/images/solidus_admin/arrow_down_s_fill_gray_700.svg b/admin/app/assets/images/solidus_admin/arrow_down_s_fill_gray_700.svg new file mode 100644 index 00000000000..78070f47c71 --- /dev/null +++ b/admin/app/assets/images/solidus_admin/arrow_down_s_fill_gray_700.svg @@ -0,0 +1,3 @@ + + + diff --git a/admin/app/assets/images/solidus_admin/arrow_down_s_fill_red_400.svg b/admin/app/assets/images/solidus_admin/arrow_down_s_fill_red_400.svg new file mode 100644 index 00000000000..de7bff77392 --- /dev/null +++ b/admin/app/assets/images/solidus_admin/arrow_down_s_fill_red_400.svg @@ -0,0 +1,3 @@ + + + diff --git a/admin/app/components/solidus_admin/ui/forms/input/component.js b/admin/app/components/solidus_admin/ui/forms/input/component.js new file mode 100644 index 00000000000..44ff79b03b0 --- /dev/null +++ b/admin/app/components/solidus_admin/ui/forms/input/component.js @@ -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('') + } +} diff --git a/admin/app/components/solidus_admin/ui/forms/input/component.rb b/admin/app/components/solidus_admin/ui/forms/input/component.rb new file mode 100644 index 00000000000..31afc2b6d64 --- /dev/null +++ b/admin/app/components/solidus_admin/ui/forms/input/component.rb @@ -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 diff --git a/admin/config/solidus_admin/tailwind.config.js.erb b/admin/config/solidus_admin/tailwind.config.js.erb index 8846b6f8ca7..5cf74942e64 100644 --- a/admin/config/solidus_admin/tailwind.config.js.erb +++ b/admin/config/solidus_admin/tailwind.config.js.erb @@ -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)', diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/input/component_preview.rb b/admin/spec/components/previews/solidus_admin/ui/forms/input/component_preview.rb new file mode 100644 index 00000000000..fe4025467fc --- /dev/null +++ b/admin/spec/components/previews/solidus_admin/ui/forms/input/component_preview.rb @@ -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 diff --git a/admin/spec/components/previews/solidus_admin/ui/forms/input/component_preview/overview.html.erb b/admin/spec/components/previews/solidus_admin/ui/forms/input/component_preview/overview.html.erb new file mode 100644 index 00000000000..33a55354b1d --- /dev/null +++ b/admin/spec/components/previews/solidus_admin/ui/forms/input/component_preview/overview.html.erb @@ -0,0 +1,28 @@ +
+

Input

+ +
+
Default
+ <%= render current_component.new(placeholder: "Placeholder") %> +
+ +
+
Filled
+ <%= render current_component.new(value: "My value") %> +
+ +
+
Error
+ <%= render current_component.new(value: "Bad value", error: "The value is wrong!") %> +
+ +
+
Disabled
+ <%= render current_component.new(placeholder: "Placeholder", disabled: true) %> +
+ +
+
Disabled filled
+ <%= render current_component.new(value: "My value", disabled: true) %> +
+
diff --git a/admin/spec/components/solidus_admin/ui/forms/input/component_spec.rb b/admin/spec/components/solidus_admin/ui/forms/input/component_spec.rb new file mode 100644 index 00000000000..cb00d6a65cb --- /dev/null +++ b/admin/spec/components/solidus_admin/ui/forms/input/component_spec.rb @@ -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