Add keyboard navigation support for board columns
- Add horizontal/vertical navigation configuration options to navigable_list controller - Support nested navigation (columns contain lists of cards) - Disable horizontal navigation within individual columns - Enable horizontal navigation at the column level - Refactor selection logic into reusable methods This allows users to navigate between columns with left/right arrow keys and between cards within a column using up/down arrow keys. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,10 @@ export default class extends Controller {
|
||||
selectionAttribute: { type: String, default: "aria-selected" },
|
||||
focusOnSelection: { type: Boolean, default: true },
|
||||
actionableItems: { type: Boolean, default: false },
|
||||
reverseNavigation: { type: Boolean, default: false }
|
||||
reverseNavigation: { type: Boolean, default: false },
|
||||
supportHorizontalNavigation: { type: Boolean, default: true },
|
||||
supportVerticalNavigation: { type: Boolean, default: true },
|
||||
hasNestedNavigation: { type: Boolean, default: false }
|
||||
}
|
||||
|
||||
connect() {
|
||||
@@ -73,24 +76,32 @@ export default class extends Controller {
|
||||
|
||||
async #setCurrentFrom(element) {
|
||||
const selectedItem = this.#visibleItems.find(item => item.contains(element))
|
||||
const id = selectedItem?.getAttribute("id")
|
||||
|
||||
if (selectedItem) {
|
||||
this.#clearSelection()
|
||||
selectedItem.setAttribute(this.selectionAttributeValue, "true")
|
||||
this.currentItem = selectedItem
|
||||
await nextFrame()
|
||||
try {
|
||||
this.currentItem.scrollIntoView({ block: "nearest", inline: "nearest" })
|
||||
} catch (e) {}
|
||||
|
||||
if (this.focusOnSelectionValue) { this.currentItem.focus() }
|
||||
if (this.hasInputTarget && id) {
|
||||
this.inputTarget.setAttribute("aria-activedescendant", id)
|
||||
}
|
||||
await this.#selectItem(selectedItem)
|
||||
}
|
||||
}
|
||||
|
||||
async #selectItem(item) {
|
||||
this.#clearSelection()
|
||||
item.setAttribute(this.selectionAttributeValue, "true")
|
||||
this.currentItem = item
|
||||
|
||||
await nextFrame()
|
||||
|
||||
this.#scrollAndFocusOnSelectedItem()
|
||||
}
|
||||
|
||||
#scrollAndFocusOnSelectedItem() {
|
||||
const id = this.currentItem?.getAttribute("id")
|
||||
this.currentItem.scrollIntoView({ block: "nearest", inline: "nearest" })
|
||||
if (this.focusOnSelectionValue) { this.currentItem.focus() }
|
||||
if (this.hasInputTarget && id) {
|
||||
this.inputTarget.setAttribute("aria-activedescendant", id)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#clearSelection() {
|
||||
for (const item of this.itemTargets) {
|
||||
item.removeAttribute(this.selectionAttributeValue)
|
||||
@@ -128,18 +139,26 @@ export default class extends Controller {
|
||||
|
||||
#keyHandlers = {
|
||||
ArrowDown(event) {
|
||||
const selectMethod = this.reverseNavigationValue ? this.#selectPrevious.bind(this) : this.#selectNext.bind(this)
|
||||
this.#handleArrowKey(event, selectMethod)
|
||||
if (this.supportVerticalNavigationValue) {
|
||||
const selectMethod = this.reverseNavigationValue ? this.#selectPrevious.bind(this) : this.#selectNext.bind(this)
|
||||
this.#handleArrowKey(event, selectMethod)
|
||||
}
|
||||
},
|
||||
ArrowUp(event) {
|
||||
const selectMethod = this.reverseNavigationValue ? this.#selectNext.bind(this) : this.#selectPrevious.bind(this)
|
||||
this.#handleArrowKey(event, selectMethod)
|
||||
if (this.supportVerticalNavigationValue) {
|
||||
const selectMethod = this.reverseNavigationValue ? this.#selectNext.bind(this) : this.#selectPrevious.bind(this)
|
||||
this.#handleArrowKey(event, selectMethod)
|
||||
}
|
||||
},
|
||||
ArrowRight(event) {
|
||||
this.#handleArrowKey(event, this.#selectNext.bind(this), false)
|
||||
if (this.supportHorizontalNavigationValue) {
|
||||
this.#handleArrowKey(event, this.#selectNext.bind(this), false)
|
||||
}
|
||||
},
|
||||
ArrowLeft(event) {
|
||||
this.#handleArrowKey(event, this.#selectPrevious.bind(this), false)
|
||||
if (this.supportHorizontalNavigationValue) {
|
||||
this.#handleArrowKey(event, this.#selectPrevious.bind(this), false)
|
||||
}
|
||||
},
|
||||
Enter(event) {
|
||||
if (event.shiftKey) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
data-drag-and-strum-target="container"
|
||||
data-collapsible-columns-target="column"
|
||||
data-navigable-list-target="item"
|
||||
data-navigable-list-support-horizontal-navigation-value="false"
|
||||
data-column-name="Done"
|
||||
data-action="turbo:before-morph-attribute->collapsible-columns#preventToggle focus->collapsible-columns#focusOnColumn"
|
||||
data-drag-and-drop-url="<%= columns_card_drops_closure_path("__id__") %>"
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
data-drag-and-strum-target="container"
|
||||
data-collapsible-columns-target="column"
|
||||
data-navigable-list-target="item"
|
||||
data-navigable-list-support-horizontal-navigation-value="false"
|
||||
data-controller="clicker"
|
||||
data-column-name="<%= column.name %>"
|
||||
data-action="turbo:before-morph-attribute->collapsible-columns#preventToggle turbo:before-stream-render@document->collapsible-columns#restoreState focus->collapsible-columns#focusOnColumn"
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
collapsible_columns_collapsed_class: "is-collapsed",
|
||||
collapsible_columns_no_transitions_class: "no-transitions",
|
||||
collapsible_columns_title_not_visible_class: "is-off-screen",
|
||||
navigable_list_support_vertical_navigation_value: false,
|
||||
navigable_list_has_nested_navigation_value: true,
|
||||
action: "
|
||||
keydown->navigable-list#navigate
|
||||
dragstart->drag-and-drop#dragStart
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
data-drag-and-drop-target="container"
|
||||
data-drag-and-strum-target="container"
|
||||
data-navigable-list-target="item"
|
||||
data-navigable-list-support-horizontal-navigation-value="false"
|
||||
data-column-name="Not Now"
|
||||
data-action="turbo:before-morph-attribute->collapsible-columns#preventToggle focus->collapsible-columns#focusOnColumn"
|
||||
data-drag-and-drop-url="<%= columns_card_drops_not_now_path("__id__") %>"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
tabindex="0"
|
||||
data-drag-and-drop-target="container"
|
||||
data-navigable-list-target="item"
|
||||
data-navigable-list-support-horizontal-navigation-value="false"
|
||||
data-column-name="Maybe?"
|
||||
data-action="turbo:before-morph-attribute->collapsible-columns#preventToggle"
|
||||
data-drag-and-drop-url="<%= columns_card_drops_stream_path("__id__") %>">
|
||||
|
||||
Reference in New Issue
Block a user