From 37fd2b248d84069e8f383a31f597fa638c15dd30 Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 2 Jun 2025 14:51:03 +0200 Subject: [PATCH] Revamp filters menu (WIP) - Make them navigable by keyboard (UP, DOWN, ENTER to activate option). - Filter based on the actual text in the element, use containment condition instead of CSS selector on value prefix. - Make all the elements in the filter searchable and reachable by keyboard. --- app/assets/stylesheets/popup.css | 4 ++ app/assets/stylesheets/terminals.css | 4 +- .../controllers/filter_controller.js | 42 +++---------- .../controllers/navigable_list_controller.js | 62 ++++++++++++++----- app/views/commands/_form.html.erb | 2 +- app/views/commands/_terminal.html.erb | 1 + app/views/events/_filter.html.erb | 21 ++++--- .../filter/_all_collections_option.html.erb | 6 +- .../events/filter/_collection_option.html.erb | 2 +- .../filter/_custom_collections.html.erb | 8 +-- .../filter/_new_collection_option.html.erb | 2 +- 11 files changed, 86 insertions(+), 68 deletions(-) diff --git a/app/assets/stylesheets/popup.css b/app/assets/stylesheets/popup.css index 3b376c072..b97f243af 100644 --- a/app/assets/stylesheets/popup.css +++ b/app/assets/stylesheets/popup.css @@ -48,6 +48,10 @@ } } + &[aria-selected] { + background: var(--color-selected); + } + &:has(:focus-visible) { background: var(--color-selected-dark); diff --git a/app/assets/stylesheets/terminals.css b/app/assets/stylesheets/terminals.css index b709ad698..f5a97bf0a 100644 --- a/app/assets/stylesheets/terminals.css +++ b/app/assets/stylesheets/terminals.css @@ -113,7 +113,7 @@ &:focus::placeholder { opacity: 0.7; } - + .terminal--confirmation & { color: var(--color-terminal-text-light); } @@ -145,7 +145,7 @@ } .terminal__item { - &[aria-current] { + &[aria-selected] { --btn-color: var(--color-terminal-bg); background: var(--color-terminal-text); diff --git a/app/javascript/controllers/filter_controller.js b/app/javascript/controllers/filter_controller.js index 815b05952..9450f203f 100644 --- a/app/javascript/controllers/filter_controller.js +++ b/app/javascript/controllers/filter_controller.js @@ -2,45 +2,21 @@ import { Controller } from "@hotwired/stimulus" import { debounce } from "helpers/timing_helpers" export default class extends Controller { - static targets = [ "list" ] - static classes = [ "active", "selected" ] + static targets = [ "input", "item" ] initialize() { this.filter = debounce(this.filter.bind(this), 300) } - connect() { - this.element.focus() - } - - filter(event) { - this.#reset() - - if (event.target.value != "") { - this.#selectMatches(event.target.value) - this.#activate() - } - } - - #reset() { - this.#deactivate() - - this.listTarget.querySelectorAll(`.${this.selectedClass}`).forEach((element) => { - element.classList.remove(this.selectedClass) + filter() { + this.itemTargets.forEach(item => { + if (item.innerText.toLowerCase().includes(this.inputTarget.value.toLowerCase())) { + item.removeAttribute("hidden") + } else { + item.toggleAttribute("hidden", true) + } }) - } - #activate() { - this.listTarget.classList.add(this.activeClass) - } - - #deactivate() { - this.listTarget.classList.remove(this.activeClass) - } - - #selectMatches(value) { - this.listTarget.querySelectorAll(`[data-value*=${value.toLowerCase()}]`).forEach((element) => { - element.classList.add(this.selectedClass) - }) + this.dispatch("changed") } } diff --git a/app/javascript/controllers/navigable_list_controller.js b/app/javascript/controllers/navigable_list_controller.js index 44f3faef6..8899f41f2 100644 --- a/app/javascript/controllers/navigable_list_controller.js +++ b/app/javascript/controllers/navigable_list_controller.js @@ -3,57 +3,81 @@ import { nextFrame } from "helpers/timing_helpers" export default class extends Controller { static targets = [ "item" ] - static values = { selectionAttribute: { type: String, default: "aria-selected" } } + static values = { + reverseOrder: { type: Boolean, default: false }, + selectionAttribute: { type: String, default: "aria-selected" }, + focusOnSelection: { type: Boolean, default: true }, + actionableItems: { type: Boolean, default: false } + } connect() { - this.selectLast() + this.reset() } // Actions - navigate(event) { - if (this.itemTargets.includes(event.target)) { - this.#keyHandlers[event.key]?.call(this, event) + reset(event) { + console.debug("SE LLEGA REVERSE", this.reverseOrderValue); + if (this.reverseOrderValue) { + this.selectLast() + } else { + this.selectFirst() } } + navigate(event) { + this.#keyHandlers[event.key]?.call(this, event) + } + select({ target }) { this.#setCurrentFrom(target) } - selectCurrentOrLast(event) { + selectCurrentOrReset(event) { if (this.currentItem) { this.#setCurrentFrom(this.currentItem) } else { - this.selectLast() + this.reset() } } + selectFirst() { + this.#setCurrentFrom(this.#visibleItems[0]) + } + selectLast() { - this.#setCurrentFrom(this.itemTargets[this.itemTargets.length - 1]) + this.#setCurrentFrom(this.#visibleItems[this.#visibleItems.length - 1]) + } + + // Private + + get #visibleItems() { + return this.itemTargets.filter(item => !item.hidden) } #selectPrevious() { - if (this.currentItem.previousElementSibling) { - this.#setCurrentFrom(this.currentItem.previousElementSibling) + const index = this.#visibleItems.indexOf(this.currentItem) + if (index > 0) { + this.#setCurrentFrom(this.#visibleItems[index - 1]) } } #selectNext() { - if (this.currentItem.nextElementSibling) { - this.#setCurrentFrom(this.currentItem.nextElementSibling) + const index = this.#visibleItems.indexOf(this.currentItem) + if (index >= 0 && index < this.#visibleItems.length - 1) { + this.#setCurrentFrom(this.#visibleItems[index + 1]) } } async #setCurrentFrom(element) { - const selectedItem = this.itemTargets.find(item => item.contains(element)) + const selectedItem = this.#visibleItems.find(item => item.contains(element)) if (selectedItem) { this.#clearSelection() selectedItem.setAttribute(this.selectionAttributeValue, "true") this.currentItem = selectedItem await nextFrame() - this.currentItem.focus() + if (this.focusOnSelectionValue) { this.currentItem.focus() } } } @@ -69,6 +93,13 @@ export default class extends Controller { event.preventDefault() } + #triggerActionOnCurrentItem() { + if (this.actionableItemsValue && this.currentItem) { + const clickableElement = this.currentItem.querySelector("a,button") || this.currentItem + clickableElement.click() + } + } + #keyHandlers = { ArrowDown(event) { this.#handleArrowKey(event, this.#selectNext.bind(this)) @@ -81,6 +112,9 @@ export default class extends Controller { }, ArrowLeft(event) { this.#handleArrowKey(event, this.#selectPrevious.bind(this)) + }, + Enter(event) { + this.#triggerActionOnCurrentItem() } } } diff --git a/app/views/commands/_form.html.erb b/app/views/commands/_form.html.erb index 68e49081e..8e8cc543f 100644 --- a/app/views/commands/_form.html.erb +++ b/app/views/commands/_form.html.erb @@ -15,7 +15,7 @@ class: "terminal__input input fill-transparent unpad", data: { terminal_target: "input", - action: "keydown.up->toggle-class#add:prevent keydown.up->navigable-list#selectCurrentOrLast terminal#hideError" + action: "keydown.up->toggle-class#add:prevent keydown.up->navigable-list#selectCurrentOrReset:stop terminal#hideError" }, placeholder: "Press #{ hotkey_label(["ctrl", "K"]) } to search or type commands…", spellcheck: "false" %> diff --git a/app/views/commands/_terminal.html.erb b/app/views/commands/_terminal.html.erb index f17f5b920..b649e7c1e 100644 --- a/app/views/commands/_terminal.html.erb +++ b/app/views/commands/_terminal.html.erb @@ -9,6 +9,7 @@ terminal_output_class: "terminal--showing-output", terminal_busy_class: "terminal--busy", toggle_class_toggle_class: "terminal--open", + navigable_list_reverse_order_value: true, action: "keydown->terminal#handleKeyPress keydown->navigable-list#navigate focusin->navigable-list#select keydown.esc->toggle-class#remove:stop keydown.esc->terminal#focus keydown.esc->navigable-list#selectLast keydown.esc@document->terminal#hideMenus" } do %> <%= turbo_frame_tag :recent_commands, src: commands_path, data: { terminal_target: "recentCommands" } %>
diff --git a/app/views/events/_filter.html.erb b/app/views/events/_filter.html.erb index 38f13feb0..38cfbb918 100644 --- a/app/views/events/_filter.html.erb +++ b/app/views/events/_filter.html.erb @@ -9,19 +9,22 @@ <% end %> - <%= tag.dialog class: "events__popup popup popup--animated panel flex-column align-start gap-half fill-white shadow", - style: "z-index: 5;", - data: { - action: "turbo:before-cache@document->dialog#close", - controller: "filter", + <%= tag.dialog class: "events__popup popup popup--animated panel flex-column align-start gap-half fill-white shadow", + style: "z-index: 5;", + data: { + action: "turbo:before-cache@document->dialog#close keydown->navigable-list#navigate filter:changed->navigable-list#reset", + controller: "filter navigable-list", dialog_target: "dialog", - filter_active_class: "filter--active", + navigable_list_focus_on_selection_value: false, + navigable_list_actionable_items_value: true, + filter_active_class: "filter--active", filter_selected_class: "selected" } do %> <%= render "events/filter/header" %> <%= render "events/filter/new_collection_option" %> - <% if Current.user.collections.count > 15 %> - - <% end %> + + <%= text_field_tag :search, nil, placeholder: "Find a collection…", class: "input input--transparent txt-small", autofocus: true, + type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %> + <%= render "events/filter/custom_collections" %> <%= form_with url: events_path, method: :get, class: "flex flex-column max-width popup__list full-width", data: { controller: "form" } do |form| %> diff --git a/app/views/events/filter/_all_collections_option.html.erb b/app/views/events/filter/_all_collections_option.html.erb index 0f57357c9..3f5e2566b 100644 --- a/app/views/events/filter/_all_collections_option.html.erb +++ b/app/views/events/filter/_all_collections_option.html.erb @@ -1,12 +1,12 @@ <% if Current.user.collections.one? %> - <%= link_to cards_path(filter.as_params.except(:collection_ids)), class: "popup__group flex align-center", style: "--hover-size: 0" do %> + <%= link_to cards_path(filter.as_params.except(:collection_ids)), class: "popup__group flex align-center", style: "--hover-size: 0", data: { filter_target: "item", navigable_list_target: "item" } do %> <%= tag.div class: "btn popup__item min-width flex-item-grow" do %> <%= Current.user.collections.first.name %> GO TO <% end %> <% end %> <% elsif Current.user.collections.many? %> -