diff --git a/app/assets/stylesheets/filters.css b/app/assets/stylesheets/filters.css index d77663e5e..7701e72c9 100644 --- a/app/assets/stylesheets/filters.css +++ b/app/assets/stylesheets/filters.css @@ -10,6 +10,12 @@ } } + .filter { + &[aria-selected] { + display: flex; + } + } + .filter__button { --btn-border-size: 0; --btn-font-weight: 400; @@ -112,10 +118,3 @@ } } -.filter--active li { - display: none; - - &.selected { - display: flex; - } -} diff --git a/app/assets/stylesheets/popup.css b/app/assets/stylesheets/popup.css index 3b376c072..cbd65028e 100644 --- a/app/assets/stylesheets/popup.css +++ b/app/assets/stylesheets/popup.css @@ -44,16 +44,15 @@ @media (any-hover: hover) { &:hover { + --hover-size: 0; + background: var(--color-selected); } } - &:has(:focus-visible) { - background: var(--color-selected-dark); - - .popup__item { - --outline-size: 0; - } + &[aria-selected] { + border-radius: 0.3em; + box-shadow: 0 0 0 var(--hover-size) var(--color-selected-dark) inset; } } @@ -77,6 +76,11 @@ } } + &[aria-selected] { + border-radius: 0.3em; + box-shadow: 0 0 0 var(--hover-size) var(--color-selected-dark) inset; + } + &:has(input:checked) { --btn-background: var(--color-ink-lightest); --btn-color: var(--color-ink); @@ -92,6 +96,7 @@ @media (any-hover: hover) { &:where(:not(:active):hover) { + --hover-size: 0; --btn-background: var(--color-selected); } } 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..98c6843bc 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) + this.filter = debounce(this.filter.bind(this), 100) } - 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 9b96c1b08..0a610a457 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-current" } } + 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) { + if (this.reverseOrderValue) { + this.selectLast() + } else { + this.selectFirst() } } + navigate(event) { + const key = event.key === " " ? "Space" : event.key + this.#keyHandlers[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,25 @@ export default class extends Controller { event.preventDefault() } + #clickCurrentItem(event) { + if (this.actionableItemsValue && this.currentItem && this.#visibleItems.length) { + const clickableElement = this.currentItem.querySelector("a,button") || this.currentItem + clickableElement.click() + event.preventDefault() + } + } + + #toggleCurrentItem(event) { + if (this.actionableItemsValue && this.currentItem && this.#visibleItems.length) { + const toggleable = this.currentItem.querySelector("input[type=checkbox]") + if (toggleable) { + toggleable.checked = !toggleable.checked + toggleable.dispatchEvent(new Event('change', { bubbles: true })) + event.preventDefault() + } + } + } + #keyHandlers = { ArrowDown(event) { this.#handleArrowKey(event, this.#selectNext.bind(this)) @@ -81,6 +124,12 @@ export default class extends Controller { }, ArrowLeft(event) { this.#handleArrowKey(event, this.#selectPrevious.bind(this)) + }, + Enter(event) { + this.#clickCurrentItem(event) + }, + Space(event) { + this.#toggleCurrentItem(event) } } } diff --git a/app/views/cards/assignments/new.html.erb b/app/views/cards/assignments/new.html.erb index 83fbeea7e..a5abad19c 100644 --- a/app/views/cards/assignments/new.html.erb +++ b/app/views/cards/assignments/new.html.erb @@ -1,26 +1,28 @@ <%= turbo_frame_tag @card, :assignment do %> - <%= tag.div class: "max-width",data: { - controller: "filter", + <%= tag.div class: "max-width", 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", - filter_selected_class: "selected"} do %> - Assign this to… + navigable_list_focus_on_selection_value: false, + navigable_list_actionable_items_value: true } do %> + Assign this to… <% if @collection.users.active.count > 1 %> - + <%= text_field_tag :search, nil, placeholder: "Filter…", class: "input input--transparent txt-small margin-block-half", autofocus: true, + type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %> <% end %> - + + <% end %> <% end %> diff --git a/app/views/cards/index/_collections_filter.html.erb b/app/views/cards/index/_collections_filter.html.erb index b735a29c8..686c9e6df 100644 --- a/app/views/cards/index/_collections_filter.html.erb +++ b/app/views/cards/index/_collections_filter.html.erb @@ -5,15 +5,15 @@ <% end %> <% if Current.user.collections.one? %> - <%= link_to cards_path(filter.as_params.except(:collection_ids)), class: "popup__group flex align-center" do %> + <%= link_to cards_path(filter.as_params.except(:collection_ids)), class: "popup__group flex align-center", data: { value: Current.user.collections.first.name.downcase, 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? %> - <%= link_to cards_path(filter.as_params.except(:collection_ids)), class: "popup__group flex align-center", style: "--hover-size: 0" do %> -