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:
Mike Lloyd
2026-04-04 13:29:43 +02:00
committed by GitHub
parent 94e7f7f0a0
commit 281197f918
4 changed files with 100 additions and 13 deletions
@@ -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 -2
View File
@@ -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>
+12 -11
View File
@@ -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),