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.
This commit is contained in:
@@ -48,6 +48,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
&[aria-selected] {
|
||||
background: var(--color-selected);
|
||||
}
|
||||
|
||||
&:has(:focus-visible) {
|
||||
background: var(--color-selected-dark);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" %>
|
||||
|
||||
@@ -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" } %>
|
||||
<div class="terminal__output" data-terminal-target="output"></div>
|
||||
|
||||
@@ -9,19 +9,22 @@
|
||||
</span>
|
||||
<% 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 %>
|
||||
<input placeholder="Find a collection…" class="input input--transparent txt-small" type="search" autocorrect="off" autocomplete="off" data-1p-ignore="true" data-action="input->filter#filter" autofocus>
|
||||
<% 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| %>
|
||||
|
||||
@@ -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 %>
|
||||
<span class="overflow-ellipsis"><%= Current.user.collections.first.name %></span>
|
||||
<span class="txt-ink translucent flex-item-no-shrink flex-item-justify-end"><span class="txt-x-small txt-uppercase">GO TO</span> ›</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% elsif Current.user.collections.many? %>
|
||||
<div class="popup__group flex align-center">
|
||||
<div class="popup__group flex align-center" data-filter-target="item" data-navigable-list-target="item">
|
||||
<%= link_to events_path(clear_filter: true), class: "btn txt-xx-small flex-item-no-shrink" do %>
|
||||
<%= check_box_tag "filter_collections", nil, filter.collections.blank?, class: "form-checkbox" %>
|
||||
<span class="for-screen-reader">
|
||||
@@ -20,4 +20,4 @@
|
||||
<span class="txt-ink translucent flex-item-no-shrink flex-item-justify-end"><span class="txt-x-small txt-uppercase">GO TO</span> ›</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<li class="popup__group flex align-center" data-value="<%= collection.name.downcase %>">
|
||||
<li class="popup__group flex align-center" data-value="<%= collection.name.downcase %>" data-filter-target="item" data-navigable-list-target="item">
|
||||
<label class="btn txt-xx-small">
|
||||
<%= form.check_box "collection_ids[]", {
|
||||
checked: filter.collections.include?(collection),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="flex flex-column max-width popup__list full-width">
|
||||
<div class="popup__group flex align-center full-width gap-half overflow-ellipsis">
|
||||
<div class="popup__group flex align-center full-width gap-half overflow-ellipsis" data-filter-target="item" data-navigable-list-target="item">
|
||||
<%= link_to cards_path(assignee_ids: [Current.user.id]), class: "btn popup__new popup__item", style: "view-transition-name: new-collection" do %>
|
||||
<%= icon_tag "filter" %>
|
||||
<span class="overflow-ellipsis">Assigned to me</span>
|
||||
@@ -7,7 +7,7 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="popup__group flex align-center full-width gap-half overflow-ellipsis">
|
||||
<div class="popup__group flex align-center full-width gap-half overflow-ellipsis" data-filter-target="item" data-navigable-list-target="item">
|
||||
<%= link_to cards_path(creator_ids: [Current.user.id]), class: "btn popup__new popup__item", style: "view-transition-name: new-collection" do %>
|
||||
<%= icon_tag "filter" %>
|
||||
<span class="overflow-ellipsis">Added by me</span>
|
||||
@@ -16,7 +16,7 @@
|
||||
</div>
|
||||
|
||||
<% @filters.each do |filter| %>
|
||||
<div class="popup__group flex align-center full-width gap-half overflow-ellipsis">
|
||||
<div class="popup__group flex align-center full-width gap-half overflow-ellipsis" data-filter-target="item" data-navigable-list-target="item">
|
||||
<%= link_to cards_path(**filter.as_params), class: "btn popup__new popup__item", style: "view-transition-name: new-collection" do %>
|
||||
<%= icon_tag "filter" %>
|
||||
<span class="overflow-ellipsis"><%= filter.summary %></span>
|
||||
@@ -24,4 +24,4 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="popup__group flex align-center full-width gap-half overflow-ellipsis">
|
||||
<div class="popup__group flex align-center full-width gap-half overflow-ellipsis" data-filter-target="item" data-navigable-list-target="item">
|
||||
<%= link_to new_collection_path, class: "btn popup__new popup__item", style: "view-transition-name: new-collection" do %>
|
||||
<%= icon_tag "add" %>
|
||||
<span>Add a new collection</span>
|
||||
|
||||
Reference in New Issue
Block a user