mirror of
https://github.com/we-promise/sure.git
synced 2026-08-12 11:10:20 +00:00
Fix opacity for excluded transactions and implement keyboard navigation (#1332)
* Make category selection menus opaque for excluded transactions * Allow keyboard navigation in category selection menu * Fix category transparency on mobile * Make checkbox opaque * Remove text-secondary from amount container * Submit form directly * Handle aria labels
This commit is contained in:
@@ -6,6 +6,8 @@ export default class extends Controller {
|
||||
|
||||
connect() {
|
||||
this.inputTarget.focus();
|
||||
this.highlightedIndex = -1;
|
||||
this.updateAriaActiveDescendant();
|
||||
}
|
||||
|
||||
filter() {
|
||||
@@ -30,5 +32,82 @@ export default class extends Controller {
|
||||
if (noMatchFound && this.hasEmptyMessageTarget) {
|
||||
this.emptyMessageTarget.classList.remove("hidden");
|
||||
}
|
||||
|
||||
this.highlightedIndex = -1;
|
||||
this.clearHighlights();
|
||||
this.updateAriaActiveDescendant();
|
||||
}
|
||||
|
||||
handleKeydown(event) {
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
this.highlightNext();
|
||||
} else if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
this.highlightPrevious();
|
||||
} else if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
this.selectHighlighted();
|
||||
}
|
||||
}
|
||||
|
||||
highlightNext() {
|
||||
const items = this.visibleItems;
|
||||
if (items.length === 0) return;
|
||||
|
||||
this.clearHighlights();
|
||||
this.highlightedIndex = Math.min(this.highlightedIndex + 1, items.length - 1);
|
||||
this.highlightItem(items[this.highlightedIndex]);
|
||||
this.updateAriaActiveDescendant();
|
||||
}
|
||||
|
||||
highlightPrevious() {
|
||||
const items = this.visibleItems;
|
||||
if (items.length === 0) return;
|
||||
|
||||
this.clearHighlights();
|
||||
this.highlightedIndex = Math.max(this.highlightedIndex - 1, 0);
|
||||
this.highlightItem(items[this.highlightedIndex]);
|
||||
this.updateAriaActiveDescendant();
|
||||
}
|
||||
|
||||
highlightItem(item) {
|
||||
item.classList.add("bg-container-inset-hover");
|
||||
item.setAttribute("aria-selected", "true");
|
||||
item.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
|
||||
clearHighlights() {
|
||||
this.listTarget.querySelectorAll(".filterable-item").forEach((item) => {
|
||||
item.classList.remove("bg-container-inset-hover");
|
||||
item.setAttribute("aria-selected", "false");
|
||||
});
|
||||
}
|
||||
|
||||
selectHighlighted() {
|
||||
const items = this.visibleItems;
|
||||
if (this.highlightedIndex < 0 || this.highlightedIndex >= items.length) return;
|
||||
|
||||
const item = items[this.highlightedIndex];
|
||||
const form = item.querySelector("form");
|
||||
if (form) {
|
||||
form.requestSubmit();
|
||||
}
|
||||
}
|
||||
|
||||
updateAriaActiveDescendant() {
|
||||
const items = this.visibleItems;
|
||||
if (this.highlightedIndex >= 0 && this.highlightedIndex < items.length) {
|
||||
const item = items[this.highlightedIndex];
|
||||
this.inputTarget.setAttribute("aria-activedescendant", item.id);
|
||||
} else {
|
||||
this.inputTarget.removeAttribute("aria-activedescendant");
|
||||
}
|
||||
}
|
||||
|
||||
get visibleItems() {
|
||||
return Array.from(this.listTarget.querySelectorAll(".filterable-item")).filter(
|
||||
(item) => item.style.display !== "none"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
<% is_selected = category.id === @selected_category&.id %>
|
||||
|
||||
<%= content_tag :div,
|
||||
id: dom_id(category, "category_option"),
|
||||
role: "option",
|
||||
aria_selected: is_selected.to_s,
|
||||
class: ["filterable-item flex justify-between items-center border-none rounded-lg px-2 py-1 group w-full hover:bg-container-inset-hover",
|
||||
{ "bg-container-inset": is_selected }],
|
||||
data: { filter_name: category.name } do %>
|
||||
|
||||
@@ -6,13 +6,17 @@
|
||||
placeholder="<%= t(".search_placeholder") %>"
|
||||
autocomplete="nope"
|
||||
type="search"
|
||||
role="combobox"
|
||||
aria-label="<%= t(".search_placeholder") %>"
|
||||
aria-expanded="false"
|
||||
aria-autocomplete="list"
|
||||
class="bg-container placeholder:text-sm placeholder:text-secondary font-normal h-10 relative pl-10 w-full border-none rounded-lg focus:outline-hidden focus:ring-0"
|
||||
data-list-filter-target="input"
|
||||
data-action="list-filter#filter">
|
||||
data-action="list-filter#filter keydown->list-filter#handleKeydown">
|
||||
<%= icon("search", class: "absolute inset-0 ml-2 transform top-1/2 -translate-y-1/2") %>
|
||||
</div>
|
||||
</div>
|
||||
<div data-list-filter-target="list" class="flex flex-col gap-0.5 p-1.5 mt-0.5 mr-2 max-h-64 overflow-y-scroll scrollbar">
|
||||
<div data-list-filter-target="list" role="listbox" class="flex flex-col gap-0.5 p-1.5 mt-0.5 mr-2 max-h-64 overflow-y-scroll scrollbar">
|
||||
<div class="pb-2 pl-4 mr-2 text-secondary hidden" data-list-filter-target="emptyMessage">
|
||||
<%= t(".no_categories") %>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<%= turbo_frame_tag dom_id(entry) do %>
|
||||
<%= turbo_frame_tag dom_id(transaction) do %>
|
||||
<div class="group flex lg:grid lg:grid-cols-12 items-center text-primary text-sm font-medium p-3 lg:p-4 <%= entry.excluded ? "opacity-50 text-secondary" : "" %> <%= "pl-8 lg:pl-12" if in_split_group %>">
|
||||
<div class="group flex lg:grid lg:grid-cols-12 items-center text-primary text-sm font-medium p-3 lg:p-4 <%= "pl-8 lg:pl-12" if in_split_group %>">
|
||||
|
||||
<div class="pr-4 lg:pr-10 flex items-center gap-3 lg:gap-4 col-span-8 min-w-0">
|
||||
<%= check_box_tag dom_id(entry, "selection"),
|
||||
@@ -17,7 +17,16 @@
|
||||
checkbox_toggle_target: "selectionEntry"
|
||||
} %>
|
||||
|
||||
<div class="max-w-full">
|
||||
<div class="flex md:hidden items-center gap-1 col-span-2 relative shrink-0">
|
||||
<%= render "transactions/transaction_category", transaction: transaction, variant: "mobile" %>
|
||||
<% if transaction.merchant&.logo_url.present? %>
|
||||
<%= image_tag Setting.transform_brand_fetch_url(transaction.merchant.logo_url),
|
||||
class: "w-5 h-5 rounded-full absolute -bottom-1 -right-1 border border-secondary pointer-events-none",
|
||||
loading: "lazy" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="max-w-full <%= 'opacity-50 text-secondary' if entry.excluded %>">
|
||||
<%= content_tag :div, class: ["flex items-center gap-3 lg:gap-4"] do %>
|
||||
<div class="hidden lg:flex">
|
||||
<% if transaction.merchant&.logo_url.present? %>
|
||||
@@ -35,14 +44,6 @@
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex md:hidden items-center gap-1 col-span-2 relative">
|
||||
<%= render "transactions/transaction_category", transaction: transaction, variant: "mobile" %>
|
||||
<% if transaction.merchant&.logo_url.present? %>
|
||||
<%= image_tag Setting.transform_brand_fetch_url(transaction.merchant.logo_url),
|
||||
class: "w-5 h-5 rounded-full absolute -bottom-1 -right-1 border border-secondary pointer-events-none",
|
||||
loading: "lazy" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="truncate">
|
||||
<div class="space-y-0.5">
|
||||
@@ -158,7 +159,7 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="shrink-0 col-span-4 lg:col-span-2 ml-auto flex items-center justify-end gap-2">
|
||||
<div class="shrink-0 col-span-4 lg:col-span-2 ml-auto flex items-center justify-end gap-2 <%= 'opacity-50' if entry.excluded %>">
|
||||
<%# Protection indicator - shows on hover when entry is protected from sync %>
|
||||
<% if entry.protected_from_sync? && !entry.excluded? %>
|
||||
<%= link_to entry_path(entry),
|
||||
|
||||
Reference in New Issue
Block a user