Files
sure/app/components/DS/select.rb
Tao Chen 2658c36b05 feat(select): improve merchant dropdown behavior and placement controls (#1364)
* feat(select): improve merchant dropdown behavior and placement controls

 - add configurable menu_placement strategy to DS::Select (auto/down/up) with safe normalization
forward menu_placement through StyledFormBuilder#collection_select
 - force Merchant dropdown to open downward in transaction create and editor forms
 - fix select option/search text contrast by applying text-primary in DS select menu
 - prevent form jump on open by scrolling only inside dropdown content instead of using scrollIntoView
 - clamp internal dropdown scroll to valid bounds for stability
- refactor select controller placement logic for readability (placementMode, clamp) without changing behavior

* set menu_placement=auto for metchant selector
2026-04-07 20:52:14 +02:00

91 lines
2.6 KiB
Ruby

module DS
class Select < ViewComponent::Base
attr_reader :form, :method, :items, :selected_value, :placeholder, :variant, :searchable, :menu_placement, :options
VARIANTS = %i[simple logo badge].freeze
MENU_PLACEMENTS = %w[auto down up].freeze
HEX_COLOR_REGEX = /\A#[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?\z/
RGB_COLOR_REGEX = /\Argb\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*\)\z/
DEFAULT_COLOR = "#737373"
def initialize(form:, method:, items:, selected: nil, placeholder: I18n.t("helpers.select.default_label"), variant: :simple, include_blank: nil, searchable: false, menu_placement: :auto, **options)
@form = form
@method = method
@placeholder = placeholder
@variant = variant
@searchable = searchable
@menu_placement = normalize_menu_placement(menu_placement)
@options = options
normalized_items = normalize_items(items)
if include_blank
normalized_items.unshift({
value: nil,
label: include_blank,
object: nil
})
end
@items = normalized_items
@selected_value = selected
end
def selected_item
items.find { |item| item[:value] == selected_value }
end
# Returns the color for a given item (used in :badge variant)
def color_for(item)
obj = item[:object]
color = obj&.respond_to?(:color) ? obj.color : DEFAULT_COLOR
return DEFAULT_COLOR unless color.is_a?(String)
if color.match?(HEX_COLOR_REGEX) || color.match?(RGB_COLOR_REGEX)
color
else
DEFAULT_COLOR
end
end
# Returns the lucide_icon name for a given item (used in :badge variant)
def icon_for(item)
obj = item[:object]
obj&.respond_to?(:lucide_icon) ? obj.lucide_icon : nil
end
# Returns true if the item has a logo (used in :logo variant)
def logo_for(item)
obj = item[:object]
obj&.respond_to?(:logo_url) && obj.logo_url.present? ? Setting.transform_brand_fetch_url(obj.logo_url) : nil
end
private
def normalize_menu_placement(value)
normalized = value.to_s.downcase
MENU_PLACEMENTS.include?(normalized) ? normalized : "auto"
end
def normalize_items(collection)
collection.map do |item|
case item
when Hash
{
value: item[:value],
label: item[:label],
object: item[:object]
}
else
{
value: item.id,
label: item.name,
object: item
}
end
end
end
end
end