@auroratide/reorder-list
Version:
Web component for a rearrangeable list of items
103 lines (101 loc) • 3.6 kB
JavaScript
import { changeEvent } from "./events.js";
import { ReorderItemElement } from "./reorder-item.js";
export class ReorderListElement extends HTMLElement {
static defaultElementName = "reorder-list";
static html = `
<slot></slot>
`;
static css = `
:host {
display: block;
list-style: disc;
padding-left: 1em;
}
:host([orientation="horizontal"]) {
display: flex;
flex-direction: row;
list-style-position: inside;
}
`;
constructor() {
super();
this.#createRoot();
}
static get observedAttributes() {
return ["orientation"];
}
get orientation() { return this.getAttribute("orientation") ?? "vertical"; }
set orientation(value) { this.setAttribute("orientation", value); }
attributeChangedCallback(attribute, oldValue, newValue) {
this.#attributeCallbacks[attribute]?.(newValue, oldValue);
}
#attributeCallbacks = {
"orientation": () => {
this.#setAriaOrientation();
},
};
items = () => Array.from(this.querySelectorAll(`:scope > ${ReorderItemElement.defaultElementName}`));
current = () => this.querySelector(`:scope > ${ReorderItemElement.defaultElementName}[tabindex="0"]`);
connectedCallback() {
this.setAttribute("role", "listbox");
this.addEventListener("keydown", this.#handleNav);
}
reorder = (curIndex, newIndex, list = this.items()) => {
const item = list[curIndex];
if (curIndex < newIndex) {
list[newIndex].after(item);
}
else {
this.insertBefore(item, list[newIndex]);
}
this.dispatchEvent(changeEvent(item, curIndex, newIndex));
list[curIndex].focus();
};
changeFocus = (newItem, list = this.items()) => {
list.forEach((item) => {
item.setAttribute("aria-selected", "false");
});
newItem.setAttribute("aria-selected", "true");
newItem.focus();
};
#handleNav = (e) => {
const keys = this.#keysForOrientation();
if (keys.includes(e.key)) {
const items = this.items();
items.indexOf(this.current());
let currentFocusable = items.indexOf(this.current());
if (currentFocusable < 0) {
currentFocusable = 0;
}
const nextFocusable = Math.max(0, Math.min(items.length - 1, currentFocusable + (e.key === keys[0] ? -1 : 1)));
if (e.altKey && currentFocusable !== nextFocusable) {
e.preventDefault();
e.stopPropagation();
this.reorder(currentFocusable, nextFocusable, items);
}
else if (currentFocusable !== nextFocusable) {
e.preventDefault();
e.stopPropagation();
this.changeFocus(items[nextFocusable], items);
}
}
};
#keysForOrientation = () => {
return this.orientation === "horizontal"
? ["ArrowLeft", "ArrowRight"]
: ["ArrowUp", "ArrowDown"];
};
#setAriaOrientation = () => {
this.setAttribute("aria-orientation", this.orientation);
};
#createRoot = () => {
const root = this.shadowRoot ?? this.attachShadow({ mode: "open" });
const style = document.createElement("style");
style.innerHTML = ReorderListElement.css;
const template = document.createElement("template");
template.innerHTML = ReorderListElement.html;
root.appendChild(style);
root.appendChild(template.content);
return root;
};
}