@paperbits/common
Version:
Paperbits common components.
191 lines (143 loc) • 7.36 kB
text/typescript
import * as Array from "@paperbits/common";
import { Keys } from "@paperbits/common";
import { Events } from "@paperbits/common/events";
import { AriaAttributes, AriaRoles, Attributes } from "../html";
import { Behavior, BehaviorHandle } from "./behavior";
import { init } from "knockout.validation";
const selectedClassName = "selected";
const optionElementSelector = "[role=option]:not([disabled])";
const selectedOptionElementSelector = `[${AriaAttributes.selected}]`;
const defaultTabIndex = "0";
export interface ListboxOptions {
onSelect: (selectedElement: HTMLElement) => void; // Changed from unknown to HTMLElement
}
export class ListboxBehavior implements Behavior<ListboxOptions> {
private cleanup: () => void;
constructor(private listboxElement: HTMLElement, private config: ListboxOptions) { }
public init(): void {
let activeItemIndex: number;
if (this.listboxElement.getAttribute(Attributes.Role) !== AriaRoles.Listbox) {
console.warn(`List and its child elements should have role="listbox" and role="option" attributes respectively.`);
return;
}
this.listboxElement.setAttribute(Attributes.TabIndex, defaultTabIndex);
const getOptionElements = (): HTMLElement[] => {
return Array.coerce<HTMLElement>(this.listboxElement.querySelectorAll(optionElementSelector));
};
const getActiveOptionElement = (): HTMLElement => {
const optionElements = getOptionElements();
const activeOptionElement = optionElements.find(x => x.getAttribute(Attributes.TabIndex) === defaultTabIndex);
return activeOptionElement;
};
const getSelectedOptionElement = (): HTMLElement => {
const selectedOptionElement = <HTMLElement>this.listboxElement.querySelector(selectedOptionElementSelector);
return selectedOptionElement;
};
const setActiveOption = (optionIndex: number): void => {
const optionElements = getOptionElements();
const currentActiveOption = optionElements.find(element => !!element.getAttribute(Attributes.TabIndex));
if (currentActiveOption) {
currentActiveOption.removeAttribute(Attributes.TabIndex);
}
const activeOption = optionElements[optionIndex];
activeOption.setAttribute(Attributes.TabIndex, defaultTabIndex);
activeOption.focus();
activeItemIndex = optionIndex;
};
const onKeyDown = (event: KeyboardEvent): void => {
const eventTarget = <HTMLElement>event.target;
if (!this.listboxElement.contains(eventTarget) || !eventTarget.matches(optionElementSelector)) {
return;
}
switch (event.key) {
case Keys.ArrowRight:
case Keys.ArrowDown:
event.preventDefault();
const nextIndex = activeItemIndex + 1;
const optionElements = getOptionElements();
if (nextIndex >= optionElements.length) {
return;
}
setActiveOption(nextIndex);
break;
case Keys.ArrowLeft:
case Keys.ArrowUp:
event.preventDefault();
const prevIndex = activeItemIndex - 1;
if (prevIndex < 0) {
return;
}
setActiveOption(prevIndex);
break;
case Keys.Enter:
case Keys.Space:
const selectedOptionElement = getSelectedOptionElement();
if (selectedOptionElement) {
selectedOptionElement.removeAttribute(AriaAttributes.selected);
selectedOptionElement.classList.remove(selectedClassName);
}
const activeOptionElement = getActiveOptionElement();
activeOptionElement.setAttribute(AriaAttributes.selected, "true");
activeOptionElement.classList.add(selectedClassName);
if (this.config.onSelect) {
// config.onSelect(ko.dataFor(activeOptionElement)); // Removed ko.dataFor
this.config.onSelect(activeOptionElement); // Pass the element itself
}
break;
}
};
const onContainerElementFocus = (): void => {
const optionElements = getOptionElements();
if (optionElements.length === 0) {
return;
}
const activeOptionElement = optionElements.find(x => x.getAttribute(Attributes.TabIndex) === defaultTabIndex);
if (activeOptionElement) {
activeOptionElement.focus();
return;
}
activeItemIndex = 0;
setActiveOption(activeItemIndex);
};
const onGlobalFocusChange = (event: KeyboardEvent): void => {
const eventTarget = <HTMLElement>event.target;
if (this.listboxElement.contains(eventTarget)) {
this.listboxElement.removeAttribute(Attributes.TabIndex);
}
else {
this.listboxElement.setAttribute(Attributes.TabIndex, defaultTabIndex);
}
};
const onMouseDown = (event: MouseEvent): void => {
const eventTarget = <HTMLElement>event.target;
const optionElement = <HTMLElement>eventTarget.closest(optionElementSelector);
if (!optionElement) {
return;
}
const optionElements = Array.coerce<HTMLElement>(this.listboxElement.querySelectorAll(optionElementSelector));
const activeItemIndex = optionElements.indexOf(optionElement);
setActiveOption(activeItemIndex);
if (this.config.onSelect) {
event.preventDefault();
event.stopImmediatePropagation();
// config.onSelect(ko.dataFor(optionElement)); // Removed ko.dataFor
this.config.onSelect(optionElement); // Pass the element itself
}
};
this.listboxElement.addEventListener(Events.MouseDown, onMouseDown, true);
this.listboxElement.addEventListener(Events.KeyDown, onKeyDown, true);
this.listboxElement.addEventListener(Events.Focus, onContainerElementFocus);
document.addEventListener(Events.Focus, onGlobalFocusChange, true);
this.cleanup = () => {
this.listboxElement.removeEventListener(Events.MouseDown, onMouseDown, true);
this.listboxElement.removeEventListener(Events.KeyDown, onKeyDown, true);
this.listboxElement.removeEventListener(Events.Focus, onContainerElementFocus);
document.removeEventListener(Events.Focus, onGlobalFocusChange, true);
};
}
public dispose(): void {
if (this.cleanup) {
this.cleanup();
}
}
}