mirror of
https://github.com/we-promise/sure.git
synced 2026-04-07 14:31:25 +00:00
New select component (#1071)
* feat: add new UI component to display dropdown select with filter * feat: use new dropdown componet for category selection in transactions * feat: improve dropdown controller * feat: Add checkbox indicator to highlight selected element in list * feat: add possibility to define dropdown without search * feat: initial implementation of variants * feat: Add default color for dropdown menu * feat: add "icon" variant for dropdown * refactor: component + controller refactoring * refactor: view + component * fix: adjust min width in selection for mobile * feat: refactor collection_select method to use new filter dropdown component * fix: compute fixed position for dropdown * feat: controller improvements * lint issues * feat: add dot color if no icon is available * refactor: controller refactor + update naming for variant from icon to logo * fix: set width to 100% for select dropdown * feat: add variant to collection_select in new transaction form * fix: typo in placeholder value * fix: add back include_blank property * refactor: rename component from FilterDropdown to Select * fix: translate placeholder and keep value_method and text_method * fix: remove duplicate variable assignment * fix: translate placeholder * fix: verify color format * fix: use right autocomplete value * fix: selection issue + controller adjustments * fix: move calls to startAutoUpdate and stopAutoUpdate * Update app/javascript/controllers/select_controller.js Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Alessio Cappa <104093777+alessiocappa@users.noreply.github.com> * fix: add aria-labels * fix: pass html_options to DS::Select * fix: unnecessary closing tag * fix: use offsetvalue for position checks * fix: use right classes for dropdown transitions * include options[:prompt] in placeholder init * fix: remove unused locale key * fix: Emit a native change event after updating the input value. * fix: Guard against negative maxHeight in constrained layouts. * fix: Update test * fix: lint issues * Update test/system/transfers_test.rb Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Alessio Cappa <104093777+alessiocappa@users.noreply.github.com> * Update test/system/transfers_test.rb Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: Alessio Cappa <104093777+alessiocappa@users.noreply.github.com> * refactor: move CSS class for button select form in maybe-design-system.css --------- Signed-off-by: Alessio Cappa <104093777+alessiocappa@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -368,12 +368,14 @@
|
||||
text-overflow: clip;
|
||||
}
|
||||
|
||||
select.form-field__input {
|
||||
select.form-field__input,
|
||||
button.form-field__input {
|
||||
@apply pr-10 appearance-none;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
||||
background-position: right -0.15rem center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1.25rem 1.25rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.form-field__radio {
|
||||
|
||||
94
app/components/DS/select.html.erb
Normal file
94
app/components/DS/select.html.erb
Normal file
@@ -0,0 +1,94 @@
|
||||
<%# locals: form:, method:, collection:, options: {} %>
|
||||
|
||||
<div class="relative" data-controller="select <%= "list-filter" if searchable %> form-dropdown" data-action="dropdown:select->form-dropdown#onSelect">
|
||||
<div class="form-field <%= options[:container_class] %>">
|
||||
<div class="form-field__body">
|
||||
<%= form.label method, options[:label], class: "form-field__label" if options[:label].present? %>
|
||||
<%= form.hidden_field method,
|
||||
value: @selected_value,
|
||||
data: {
|
||||
"form-dropdown-target": "input",
|
||||
"auto-submit-target": "auto"
|
||||
} %>
|
||||
<button type="button"
|
||||
class="form-field__input w-full"
|
||||
data-select-target="button"
|
||||
data-action="click->select#toggle"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded="<%= @selected_value.present? ? "true" : "false" %>"
|
||||
aria-labelledby="<%= "#{method}_label" %>">
|
||||
<%= selected_item&.dig(:label) || @placeholder %>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute z-50 p-1.5 w-full min-w-32 rounded-lg shadow-lg shadow-border-xs bg-container mt-1.5 transition duration-150 ease-out -translate-y-1 opacity-0 hidden" data-select-target="menu">
|
||||
<% if searchable %>
|
||||
<div class="relative flex items-center bg-container border border-secondary rounded-lg mb-1">
|
||||
<input type="search"
|
||||
placeholder="<%= t("helpers.select.search_placeholder") %>"
|
||||
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"
|
||||
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") %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div data-list-filter-target="list" data-select-target="content" class="flex flex-col gap-0.5 max-h-64 overflow-auto"
|
||||
role="listbox" tabindex="-1">
|
||||
<% items.each do |item| %>
|
||||
<% is_selected = item[:value] == selected_value %>
|
||||
<% obj = item[:object] %>
|
||||
|
||||
<div class="filterable-item text-sm cursor-pointer flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-container-inset-hover <%= "bg-container-inset" if is_selected %>"
|
||||
role="option"
|
||||
tabindex="0"
|
||||
aria-selected="<%= is_selected %>"
|
||||
data-action="click->select#select"
|
||||
data-value="<%= item[:value] %>"
|
||||
data-filter-name="<%= item[:label] %>">
|
||||
|
||||
<span class="check-icon <%= "hidden" unless is_selected %>">
|
||||
<%= helpers.icon("check") %>
|
||||
</span>
|
||||
|
||||
<% case variant %>
|
||||
<% when :simple %>
|
||||
<%= item[:label] %>
|
||||
|
||||
<% when :logo %>
|
||||
<% unless item[:value].nil? %>
|
||||
<% if logo_for(item) %>
|
||||
<%= image_tag logo_for(item),
|
||||
class: "w-6 h-6 rounded-full border border-secondary",
|
||||
loading: "lazy" %>
|
||||
<% else %>
|
||||
<%= render DS::FilledIcon.new(
|
||||
variant: :text,
|
||||
text: item[:label],
|
||||
size: "sm",
|
||||
rounded: true
|
||||
) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= item[:label] %>
|
||||
|
||||
<% when :badge %>
|
||||
<% hex_color = color_for(item) %>
|
||||
<span class="flex items-center gap-2 text-sm font-medium rounded-full px-3 py-1 border truncate"
|
||||
style="
|
||||
background-color: color-mix(in oklab, <%= hex_color %> 10%, transparent);
|
||||
border-color: color-mix(in oklab, <%= hex_color %> 20%, transparent);
|
||||
color: <%= hex_color %>;">
|
||||
<% if icon_for(item) %>
|
||||
<%= helpers.icon icon_for(item), size: "sm", color: "current" %>
|
||||
<% else %>
|
||||
<span class="size-1.5 rounded-full" style="background-color: <%= hex_color %>;"></span>
|
||||
<% end %>
|
||||
<%= item[:label] %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
83
app/components/DS/select.rb
Normal file
83
app/components/DS/select.rb
Normal file
@@ -0,0 +1,83 @@
|
||||
module DS
|
||||
class Select < ViewComponent::Base
|
||||
attr_reader :form, :method, :items, :selected_value, :placeholder, :variant, :searchable, :options
|
||||
|
||||
VARIANTS = %i[simple logo badge].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)
|
||||
@form = form
|
||||
@method = method
|
||||
@placeholder = placeholder
|
||||
@variant = variant
|
||||
@searchable = searchable
|
||||
@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_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
|
||||
@@ -28,11 +28,25 @@ class StyledFormBuilder < ActionView::Helpers::FormBuilder
|
||||
end
|
||||
|
||||
def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
|
||||
field_options = normalize_options(options, html_options)
|
||||
selected_value = @object.public_send(method) if @object.respond_to?(method)
|
||||
placeholder = options[:prompt] || options[:include_blank] || options[:placeholder] || I18n.t("helpers.select.default_label")
|
||||
|
||||
build_field(method, field_options, html_options) do |merged_html_options|
|
||||
super(method, collection, value_method, text_method, options, merged_html_options)
|
||||
end
|
||||
@template.render(
|
||||
DS::Select.new(
|
||||
form: self,
|
||||
method: method,
|
||||
items: collection.map { |item| { value: item.public_send(value_method), label: item.public_send(text_method), object: item } },
|
||||
selected: selected_value,
|
||||
placeholder: placeholder,
|
||||
searchable: options.fetch(:searchable, false),
|
||||
variant: options.fetch(:variant, :simple),
|
||||
include_blank: options[:include_blank],
|
||||
label: options[:label],
|
||||
container_class: options[:container_class],
|
||||
label_tooltip: options[:label_tooltip],
|
||||
html_options: html_options
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
def money_field(amount_method, options = {})
|
||||
|
||||
18
app/javascript/controllers/form_dropdown_controller.js
Normal file
18
app/javascript/controllers/form_dropdown_controller.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["input"]
|
||||
|
||||
onSelect(event) {
|
||||
this.inputTarget.value = event.detail.value
|
||||
|
||||
const inputEvent = new Event("input", { bubbles: true })
|
||||
this.inputTarget.dispatchEvent(inputEvent)
|
||||
|
||||
const form = this.element.closest("form")
|
||||
const controllers = (form?.dataset.controller || "").split(/\s+/)
|
||||
if (form && controllers.includes("auto-submit-form")) {
|
||||
form.requestSubmit()
|
||||
}
|
||||
}
|
||||
}
|
||||
182
app/javascript/controllers/select_controller.js
Normal file
182
app/javascript/controllers/select_controller.js
Normal file
@@ -0,0 +1,182 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import { autoUpdate } from "@floating-ui/dom"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["button", "menu", "input"]
|
||||
static values = {
|
||||
placement: { type: String, default: "bottom-start" },
|
||||
offset: { type: Number, default: 6 }
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.isOpen = false
|
||||
this.boundOutsideClick = this.handleOutsideClick.bind(this)
|
||||
this.boundKeydown = this.handleKeydown.bind(this)
|
||||
this.boundTurboLoad = this.handleTurboLoad.bind(this)
|
||||
|
||||
document.addEventListener("click", this.boundOutsideClick)
|
||||
document.addEventListener("turbo:load", this.boundTurboLoad)
|
||||
this.element.addEventListener("keydown", this.boundKeydown)
|
||||
|
||||
this.observeMenuResize()
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
document.removeEventListener("click", this.boundOutsideClick)
|
||||
document.removeEventListener("turbo:load", this.boundTurboLoad)
|
||||
this.element.removeEventListener("keydown", this.boundKeydown)
|
||||
this.stopAutoUpdate()
|
||||
if (this.resizeObserver) this.resizeObserver.disconnect()
|
||||
}
|
||||
|
||||
toggle = () => {
|
||||
this.isOpen ? this.close() : this.openMenu()
|
||||
}
|
||||
|
||||
openMenu() {
|
||||
this.isOpen = true
|
||||
this.menuTarget.classList.remove("hidden")
|
||||
this.buttonTarget.setAttribute("aria-expanded", "true")
|
||||
this.startAutoUpdate()
|
||||
this.clearSearch()
|
||||
requestAnimationFrame(() => {
|
||||
this.menuTarget.classList.remove("opacity-0", "-translate-y-1", "pointer-events-none")
|
||||
this.menuTarget.classList.add("opacity-100", "translate-y-0")
|
||||
this.updatePosition()
|
||||
this.scrollToSelected()
|
||||
})
|
||||
}
|
||||
|
||||
close() {
|
||||
this.isOpen = false
|
||||
this.stopAutoUpdate()
|
||||
this.menuTarget.classList.remove("opacity-100", "translate-y-0")
|
||||
this.menuTarget.classList.add("opacity-0", "-translate-y-1", "pointer-events-none")
|
||||
this.buttonTarget.setAttribute("aria-expanded", "false")
|
||||
setTimeout(() => { if (!this.isOpen && this.hasMenuTarget) this.menuTarget.classList.add("hidden") }, 150)
|
||||
}
|
||||
|
||||
select(event) {
|
||||
const selectedElement = event.currentTarget
|
||||
const value = selectedElement.dataset.value
|
||||
const label = selectedElement.dataset.filterName || selectedElement.textContent.trim()
|
||||
|
||||
this.buttonTarget.textContent = label
|
||||
if (this.hasInputTarget) {
|
||||
this.inputTarget.value = value
|
||||
this.inputTarget.dispatchEvent(new Event("change", { bubbles: true }))
|
||||
}
|
||||
|
||||
const previousSelected = this.menuTarget.querySelector("[aria-selected='true']")
|
||||
if (previousSelected) {
|
||||
previousSelected.setAttribute("aria-selected", "false")
|
||||
previousSelected.classList.remove("bg-container-inset")
|
||||
const prevIcon = previousSelected.querySelector(".check-icon")
|
||||
if (prevIcon) prevIcon.classList.add("hidden")
|
||||
}
|
||||
|
||||
selectedElement.setAttribute("aria-selected", "true")
|
||||
selectedElement.classList.add("bg-container-inset")
|
||||
const selectedIcon = selectedElement.querySelector(".check-icon")
|
||||
if (selectedIcon) selectedIcon.classList.remove("hidden")
|
||||
|
||||
this.element.dispatchEvent(new CustomEvent("dropdown:select", {
|
||||
detail: { value, label },
|
||||
bubbles: true
|
||||
}))
|
||||
|
||||
this.close()
|
||||
this.buttonTarget.focus()
|
||||
}
|
||||
|
||||
focusSearch() {
|
||||
const input = this.menuTarget.querySelector('input[type="search"]')
|
||||
if (input) { input.focus({ preventScroll: true }); return true }
|
||||
return false
|
||||
}
|
||||
|
||||
focusFirstElement() {
|
||||
const selector = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
const el = this.menuTarget.querySelector(selector)
|
||||
if (el) el.focus({ preventScroll: true })
|
||||
}
|
||||
|
||||
scrollToSelected() {
|
||||
const selected = this.menuTarget.querySelector(".bg-container-inset")
|
||||
if (selected) selected.scrollIntoView({ block: "center" })
|
||||
}
|
||||
|
||||
handleOutsideClick(event) {
|
||||
if (this.isOpen && !this.element.contains(event.target)) this.close()
|
||||
}
|
||||
|
||||
handleKeydown(event) {
|
||||
if (!this.isOpen) return
|
||||
if (event.key === "Escape") { this.close(); this.buttonTarget.focus() }
|
||||
if (event.key === "Enter" && event.target.dataset.value) { event.preventDefault(); event.target.click() }
|
||||
}
|
||||
|
||||
handleTurboLoad() { if (this.isOpen) this.close() }
|
||||
|
||||
clearSearch() {
|
||||
const input = this.menuTarget.querySelector('input[type="search"]')
|
||||
if (!input) return
|
||||
input.value = ""
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }))
|
||||
}
|
||||
|
||||
startAutoUpdate() {
|
||||
if (!this._cleanup && this.buttonTarget && this.menuTarget) {
|
||||
this._cleanup = autoUpdate(this.buttonTarget, this.menuTarget, () => this.updatePosition())
|
||||
}
|
||||
}
|
||||
|
||||
stopAutoUpdate() {
|
||||
if (this._cleanup) { this._cleanup(); this._cleanup = null }
|
||||
}
|
||||
|
||||
observeMenuResize() {
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
if (this.isOpen) requestAnimationFrame(() => this.updatePosition())
|
||||
})
|
||||
this.resizeObserver.observe(this.menuTarget)
|
||||
}
|
||||
|
||||
getScrollParent(element) {
|
||||
let parent = element.parentElement
|
||||
while (parent) {
|
||||
const style = getComputedStyle(parent)
|
||||
const overflowY = style.overflowY
|
||||
if (overflowY === "auto" || overflowY === "scroll") return parent
|
||||
parent = parent.parentElement
|
||||
}
|
||||
return document.documentElement
|
||||
}
|
||||
|
||||
updatePosition() {
|
||||
if (!this.buttonTarget || !this.menuTarget || !this.isOpen) return
|
||||
|
||||
const container = this.getScrollParent(this.element)
|
||||
const containerRect = container.getBoundingClientRect()
|
||||
const buttonRect = this.buttonTarget.getBoundingClientRect()
|
||||
const menuHeight = this.menuTarget.scrollHeight
|
||||
|
||||
const spaceBelow = containerRect.bottom - buttonRect.bottom
|
||||
const spaceAbove = buttonRect.top - containerRect.top
|
||||
const shouldOpenUp = spaceBelow < menuHeight && spaceAbove > spaceBelow
|
||||
|
||||
this.menuTarget.style.left = "0"
|
||||
this.menuTarget.style.width = "100%"
|
||||
this.menuTarget.style.top = ""
|
||||
this.menuTarget.style.bottom = ""
|
||||
this.menuTarget.style.overflowY = "auto"
|
||||
|
||||
if (shouldOpenUp) {
|
||||
this.menuTarget.style.bottom = "100%"
|
||||
this.menuTarget.style.maxHeight = `${Math.max(0, spaceAbove - this.offsetValue)}px`
|
||||
} else {
|
||||
this.menuTarget.style.top = "100%"
|
||||
this.menuTarget.style.maxHeight = `${Math.max(0, spaceBelow - this.offsetValue)}px`
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,13 +18,13 @@
|
||||
<% if @entry.account_id %>
|
||||
<%= f.hidden_field :account_id %>
|
||||
<% else %>
|
||||
<%= f.collection_select :account_id, Current.family.accounts.manual.active.alphabetically, :id, :name, { prompt: t(".account_prompt"), label: t(".account") }, required: true, class: "form-field__input text-ellipsis" %>
|
||||
<%= f.collection_select :account_id, Current.family.accounts.manual.active.alphabetically, :id, :name, { prompt: t(".account_prompt"), label: t(".account"), variant: :logo }, required: true, class: "form-field__input text-ellipsis" %>
|
||||
<% end %>
|
||||
|
||||
<%= f.money_field :amount, label: t(".amount"), required: true %>
|
||||
<%= f.fields_for :entryable do |ef| %>
|
||||
<% categories = params[:nature] == "inflow" ? income_categories : expense_categories %>
|
||||
<%= ef.collection_select :category_id, categories, :id, :name, { prompt: t(".category_prompt"), label: t(".category") } %>
|
||||
<%= ef.collection_select :category_id, categories, :id, :name, { prompt: t(".category_prompt"), label: t(".category"), variant: :badge, searchable: true } %>
|
||||
<% end %>
|
||||
<%= f.date_field :date, label: t(".date"), required: true, min: Entry.min_supported_date, max: Date.current, value: Date.current %>
|
||||
</section>
|
||||
|
||||
@@ -78,7 +78,8 @@
|
||||
Current.family.categories.alphabetically,
|
||||
:id, :name,
|
||||
{ label: t(".category_label"),
|
||||
class: "text-subdued", include_blank: t(".uncategorized") },
|
||||
class: "text-subdued", include_blank: t(".uncategorized"),
|
||||
variant: :badge, searchable: true },
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -104,7 +105,7 @@
|
||||
:id, :name,
|
||||
{ include_blank: t(".none"),
|
||||
label: t(".merchant_label"),
|
||||
class: "text-subdued" },
|
||||
class: "text-subdued", variant: :logo, searchable: true },
|
||||
"data-auto-submit-form-target": "auto" %>
|
||||
<%= ef.select :tag_ids,
|
||||
Current.family.tags.alphabetically.pluck(:name, :id),
|
||||
|
||||
@@ -153,6 +153,8 @@ en:
|
||||
helpers:
|
||||
select:
|
||||
prompt: Please select
|
||||
search_placeholder: "Search"
|
||||
default_label: "Select..."
|
||||
submit:
|
||||
create: Create %{model}
|
||||
submit: Save %{model}
|
||||
|
||||
@@ -12,20 +12,41 @@ class TransfersTest < ApplicationSystemTestCase
|
||||
transfer_date = Date.current
|
||||
|
||||
click_on "New transaction"
|
||||
|
||||
# Will navigate to different route in same modal
|
||||
click_on "Transfer"
|
||||
assert_text "New transfer"
|
||||
|
||||
select checking_name, from: "From"
|
||||
select savings_name, from: "To"
|
||||
# Select accounts using DS::Select
|
||||
select_ds("From", checking_name)
|
||||
select_ds("To", savings_name)
|
||||
|
||||
fill_in "transfer[amount]", with: 500
|
||||
fill_in "Date", with: transfer_date
|
||||
|
||||
click_button "Create transfer"
|
||||
|
||||
within "#entry-group-" + transfer_date.to_s do
|
||||
within "#entry-group-#{transfer_date}" do
|
||||
assert_text "Payment to"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def select_ds(label_text, option_text)
|
||||
field_label = find("label", exact_text: label_text)
|
||||
container = field_label.ancestor("div.relative")
|
||||
|
||||
# Click the button to open the dropdown
|
||||
container.find("button").click
|
||||
|
||||
# If searchable, type in the search input
|
||||
if container.has_selector?("input[type='search']", visible: true)
|
||||
container.find("input[type='search']", visible: true).set(option_text)
|
||||
end
|
||||
|
||||
# Wait for the listbox to appear inside the relative container
|
||||
listbox = container.find("[role='listbox']", visible: true)
|
||||
|
||||
# Click the option inside the listbox
|
||||
listbox.find("[role='option']", exact_text: option_text, visible: true).click
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user