diff --git a/app/components/DS/select.html.erb b/app/components/DS/select.html.erb index 170d3ee26..3ae5ce0a6 100644 --- a/app/components/DS/select.html.erb +++ b/app/components/DS/select.html.erb @@ -1,6 +1,6 @@ <%# locals: form:, method:, collection:, options: {} %> -
form-dropdown" data-action="dropdown:select->form-dropdown#onSelect"> +
form-dropdown" data-select-menu-placement-value="<%= menu_placement %>" data-action="dropdown:select->form-dropdown#onSelect">
<%= form.label method, options[:label], class: "form-field__label" if options[:label].present? %> @@ -27,7 +27,7 @@ " autocomplete="off" - class="bg-container text-sm placeholder:text-secondary font-normal h-10 pl-10 w-full border-none rounded-lg focus:outline-hidden focus:ring-0" + class="bg-container text-primary text-sm placeholder:text-secondary font-normal h-10 pl-10 w-full border-none rounded-lg focus:outline-hidden focus:ring-0" data-list-filter-target="input" data-action="list-filter#filter"> <%= helpers.icon("search", class: "absolute inset-0 ml-2 transform top-1/2 -translate-y-1/2") %> @@ -39,7 +39,7 @@ <% is_selected = item[:value] == selected_value %> <% obj = item[:object] %> -
" +
" role="option" tabindex="0" aria-selected="<%= is_selected %>" diff --git a/app/components/DS/select.rb b/app/components/DS/select.rb index abbd48ada..b19166bfb 100644 --- a/app/components/DS/select.rb +++ b/app/components/DS/select.rb @@ -1,18 +1,20 @@ module DS class Select < ViewComponent::Base - attr_reader :form, :method, :items, :selected_value, :placeholder, :variant, :searchable, :options + 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, **options) + 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) @@ -61,6 +63,11 @@ module DS 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 diff --git a/app/helpers/styled_form_builder.rb b/app/helpers/styled_form_builder.rb index bb51ab2ff..ba3b5f81e 100644 --- a/app/helpers/styled_form_builder.rb +++ b/app/helpers/styled_form_builder.rb @@ -44,6 +44,7 @@ class StyledFormBuilder < ActionView::Helpers::FormBuilder selected: selected_value, placeholder: placeholder, searchable: options.fetch(:searchable, false), + menu_placement: options[:menu_placement], variant: options.fetch(:variant, :simple), include_blank: options[:include_blank], label: options[:label], diff --git a/app/javascript/controllers/select_controller.js b/app/javascript/controllers/select_controller.js index 23b56051d..e4b07bb3b 100644 --- a/app/javascript/controllers/select_controller.js +++ b/app/javascript/controllers/select_controller.js @@ -2,9 +2,9 @@ import { Controller } from "@hotwired/stimulus" import { autoUpdate } from "@floating-ui/dom" export default class extends Controller { - static targets = ["button", "menu", "input"] + static targets = ["button", "menu", "input", "content"] static values = { - placement: { type: String, default: "bottom-start" }, + menuPlacement: { type: String, default: "auto" }, offset: { type: Number, default: 6 } } @@ -103,7 +103,25 @@ export default class extends Controller { scrollToSelected() { const selected = this.menuTarget.querySelector(".bg-container-inset") - if (selected) selected.scrollIntoView({ block: "center" }) + if (!selected) return + + const container = this.hasContentTarget ? this.contentTarget : this.menuTarget + const containerRect = container.getBoundingClientRect() + const selectedRect = selected.getBoundingClientRect() + const delta = selectedRect.top - containerRect.top - (container.clientHeight - selectedRect.height) / 2 + + const nextScrollTop = container.scrollTop + delta + const maxScrollTop = Math.max(0, container.scrollHeight - container.clientHeight) + container.scrollTop = this.clamp(nextScrollTop, 0, maxScrollTop) + } + + clamp(value, min, max) { + return Math.min(max, Math.max(min, value)) + } + + placementMode() { + const mode = (this.menuPlacementValue || "auto").toLowerCase() + return ["auto", "down", "up"].includes(mode) ? mode : "auto" } handleOutsideClick(event) { @@ -163,7 +181,8 @@ export default class extends Controller { const spaceBelow = containerRect.bottom - buttonRect.bottom const spaceAbove = buttonRect.top - containerRect.top - const shouldOpenUp = spaceBelow < menuHeight && spaceAbove > spaceBelow + const placement = this.placementMode() + const shouldOpenUp = placement === "up" || (placement === "auto" && spaceBelow < menuHeight && spaceAbove > spaceBelow) this.menuTarget.style.left = "0" this.menuTarget.style.width = "100%" diff --git a/app/views/transactions/_form.html.erb b/app/views/transactions/_form.html.erb index 062d87c8b..2ff28e05e 100644 --- a/app/views/transactions/_form.html.erb +++ b/app/views/transactions/_form.html.erb @@ -34,7 +34,10 @@ Current.family.available_merchants_for(Current.user).alphabetically, :id, :name, { include_blank: t(".none"), - label: t(".merchant_label") } %> + label: t(".merchant_label"), + variant: :logo, + searchable: true, + menu_placement: :auto } %> <%= ef.select :tag_ids, Current.family.tags.alphabetically.pluck(:name, :id), { diff --git a/app/views/transactions/show.html.erb b/app/views/transactions/show.html.erb index 1893b3364..eb7dbb4cf 100644 --- a/app/views/transactions/show.html.erb +++ b/app/views/transactions/show.html.erb @@ -109,7 +109,7 @@ :id, :name, { include_blank: t(".none"), label: t(".merchant_label"), - class: "text-subdued", variant: :logo, searchable: true, disabled: @entry.split_child? || !can_annotate_entry? }, + variant: :logo, searchable: true, menu_placement: :auto, disabled: @entry.split_child? || !can_annotate_entry? }, "data-auto-submit-form-target": "auto" %> <%= ef.select :tag_ids, Current.family.tags.alphabetically.pluck(:name, :id),