Skip to content

Commit ceba621

Browse files
authored
Merge pull request #157 from unoforge/changes
refactor(accordion): improve keyboard navigation and trigger handling
2 parents d0a2bf3 + adf27cd commit ceba621

File tree

2 files changed

+45
-18
lines changed

2 files changed

+45
-18
lines changed

packages/accordion/src/accordion.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export default class Accordion {
2525
private eventListeners: Array<{ element: HTMLElement; type: string; listener: EventListener }> = [];
2626
private cleanupObserver: (() => void) | null = null;
2727

28+
29+
2830
/**
2931
* Creates an instance of Accordion
3032
* @param {string | HTMLElement} accordion - Selector string or HTMLElement for the accordion container
@@ -71,11 +73,9 @@ export default class Accordion {
7173
}
7274
}
7375
this.addEventListeners();
74-
const handleKeyEvents = (e: KeyboardEvent) => {
75-
initKeyEvents(e, this.accordionEl)
76-
}
77-
this.accordionEl.addEventListener("keydown", handleKeyEvents);
78-
this.eventListeners.push({ element: this.accordionEl, type: "keydown", listener: handleKeyEvents as EventListener });
76+
77+
this.accordionEl.addEventListener("keydown", this.handleKeyEvents);
78+
this.eventListeners.push({ element: this.accordionEl, type: "keydown", listener: this.handleKeyEvents as EventListener });
7979

8080
FlexillaManager.register("accordion", this.accordionEl, this)
8181

@@ -86,6 +86,10 @@ export default class Accordion {
8686
});
8787
}
8888

89+
private handleKeyEvents = (e: KeyboardEvent) => {
90+
initKeyEvents(e, this.accordionEl)
91+
}
92+
8993
reload = () => {
9094
this.cleanup()
9195
this.items = $$("[data-accordion-item]", this.accordionEl).filter((item: HTMLElement) => item.parentElement && item.parentElement === this.accordionEl);

packages/accordion/src/helpers.ts

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,49 @@ import { expandElement, collapseElement, initCollapsible } from "./../../collaps
22
import { $d, $$ } from "@flexilla/utilities/selector"
33

44
const getAdjacentTrigger = (currentTrigger: HTMLElement, goUp: boolean, accordionElement: HTMLElement) => {
5-
const allAccordionItems = $$("[data-accordion-item]", accordionElement)
6-
const accordionItems = allAccordionItems.filter((item) => item.parentElement === accordionElement);
7-
const currentTriggerIndex = Array.from(accordionItems).indexOf(currentTrigger.closest('[data-accordion-item]')!);
8-
const nextIndex = goUp ? currentTriggerIndex - 1 : currentTriggerIndex + 1;
9-
const nextTrigger = $d("[data-accordion-trigger]", accordionItems[nextIndex]) as HTMLElement
5+
// Get all direct children [data-accordion-item] of accordionElement
6+
const accordionItems = $$(`:scope > [data-accordion-item]`, accordionElement) as HTMLElement[];
107

11-
return nextTrigger ?? (goUp ? $d("[data-accordion-trigger]", accordionItems[accordionItems.length - 1]) : $d("[data-accordion-trigger]", accordionItems[0]))
12-
}
8+
// Find the current accordion item (direct parent of the trigger)
9+
const currentItem = currentTrigger.parentElement;
10+
if (!accordionItems.includes(currentItem as HTMLElement)) return null; // Not a valid direct child item
11+
12+
const currentIndex = accordionItems.indexOf(currentItem as HTMLElement);
13+
const nextIndex = goUp ? currentIndex - 1 : currentIndex + 1;
14+
15+
// Determine the next item, looping to last/first if out of bounds
16+
const nextItem =
17+
accordionItems[nextIndex] ??
18+
(goUp ? accordionItems[accordionItems.length - 1] : accordionItems[0]);
19+
20+
// Select trigger that is a direct child of the next item
21+
const nextTrigger = $d(`:scope > [data-accordion-trigger]`, nextItem);
22+
return nextTrigger instanceof HTMLElement ? nextTrigger : null;
23+
};
1324

1425
const initKeyEvents = (event: KeyboardEvent, accordionElement: HTMLElement) => {
26+
if (!(document.activeElement instanceof HTMLElement)) return;
27+
1528
const focusedTrigger = document.activeElement;
16-
if (!(focusedTrigger instanceof HTMLElement)) return
1729

18-
const isTriggerFocused = focusedTrigger.matches('[data-accordion-trigger]');
19-
if (isTriggerFocused && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) {
20-
event.preventDefault(); // Prevent default scrolling behavior
30+
// Ensure it's the right trigger in the correct structure
31+
const parentItem = focusedTrigger.parentElement;
32+
if (
33+
!focusedTrigger.matches('[data-accordion-trigger]') ||
34+
!parentItem?.matches('[data-accordion-item]') ||
35+
parentItem.parentElement !== accordionElement
36+
) {
37+
return;
38+
}
39+
40+
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
41+
event.preventDefault(); // Prevent default scrolling
2142
const nextTrigger = getAdjacentTrigger(focusedTrigger, event.key === 'ArrowUp', accordionElement);
22-
nextTrigger.focus();
43+
if (nextTrigger) {
44+
nextTrigger.focus();
45+
}
2346
}
24-
}
47+
};
2548

2649
const changeTriggerState = (trigger: HTMLElement, state: "open" | "close") => {
2750
trigger.ariaExpanded = state === "open" ? "true" : "false";

0 commit comments

Comments
 (0)