Utilities for finding and managing focusable elements in the DOM, essential for accessibility and keyboard navigation in drag and drop interfaces.
Finds the first focusable element within or including the given element. Uses a comprehensive CSS selector to identify elements that can receive keyboard focus.
/**
* Finds the first focusable element within or including the given element
* @param element - The container element to search within
* @returns The first focusable HTMLElement found, or null if none exists
*/
function findFirstFocusableNode(element: HTMLElement): HTMLElement | null;Focusable Elements Detected:
<a> with href)<input>, <select>, <textarea>, <button>) that are not disabledtabindex attributeUsage Examples:
import { findFirstFocusableNode } from "@dnd-kit/utilities";
// Basic focus management
function focusFirstElement(container: HTMLElement) {
const firstFocusable = findFirstFocusableNode(container);
if (firstFocusable) {
firstFocusable.focus();
console.log("Focused:", firstFocusable.tagName);
} else {
console.log("No focusable elements found");
}
}
// Modal dialog focus management
function setupModalFocus(modal: HTMLElement) {
// Focus first element when modal opens
const firstFocusable = findFirstFocusableNode(modal);
if (firstFocusable) {
firstFocusable.focus();
}
// Trap focus within modal
modal.addEventListener("keydown", (event) => {
if (event.key === "Tab") {
const focusableElements = getAllFocusableElements(modal);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (event.shiftKey && document.activeElement === firstElement) {
event.preventDefault();
lastElement?.focus();
} else if (!event.shiftKey && document.activeElement === lastElement) {
event.preventDefault();
firstElement?.focus();
}
}
});
}
function getAllFocusableElements(container: HTMLElement): HTMLElement[] {
const selector = 'a,frame,iframe,input:not([type=hidden]):not(:disabled),select:not(:disabled),textarea:not(:disabled),button:not(:disabled),*[tabindex]';
return Array.from(container.querySelectorAll(selector)) as HTMLElement[];
}
// Drag and drop accessibility
function setupAccessibleDragDrop(draggableContainer: HTMLElement) {
const firstFocusable = findFirstFocusableNode(draggableContainer);
if (firstFocusable) {
// Make container keyboard navigable if no focusable children
draggableContainer.tabIndex = 0;
draggableContainer.setAttribute("role", "button");
draggableContainer.setAttribute("aria-label", "Draggable item");
// Handle keyboard drag initiation
draggableContainer.addEventListener("keydown", (event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
initiateDragWithKeyboard(draggableContainer);
}
});
}
}
function initiateDragWithKeyboard(element: HTMLElement) {
console.log("Starting keyboard drag for:", element);
// Implement keyboard drag logic
}
// Form validation with focus management
function validateFormWithFocus(form: HTMLFormElement) {
const inputs = form.querySelectorAll("input, select, textarea") as NodeListOf<HTMLInputElement>;
for (const input of inputs) {
if (!input.validity.valid) {
// Focus the first invalid field
input.focus();
input.scrollIntoView({ behavior: "smooth", block: "center" });
// Highlight the error
input.setAttribute("aria-invalid", "true");
return false;
}
}
return true;
}
// React component example
import React, { useEffect, useRef } from "react";
function AccessibleDropZone({ children, onDrop }: {
children: React.ReactNode;
onDrop: (data: any) => void;
}) {
const dropZoneRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const dropZone = dropZoneRef.current;
if (!dropZone) return;
// Ensure drop zone is focusable
const firstFocusable = findFirstFocusableNode(dropZone);
if (!firstFocusable) {
dropZone.tabIndex = 0;
dropZone.setAttribute("role", "region");
dropZone.setAttribute("aria-label", "Drop zone");
}
// Handle keyboard interactions
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Enter") {
// Trigger drop action via keyboard
onDrop({ type: "keyboard-drop" });
}
};
dropZone.addEventListener("keydown", handleKeyDown);
return () => {
dropZone.removeEventListener("keydown", handleKeyDown);
};
}, [onDrop]);
return (
<div
ref={dropZoneRef}
className="drop-zone"
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => {
e.preventDefault();
onDrop({ type: "drag-drop", data: e.dataTransfer?.getData("text") });
}}
>
{children}
</div>
);
}
// Dynamic content focus management
function handleDynamicContentFocus(container: HTMLElement, newContent: HTMLElement) {
// Add new content
container.appendChild(newContent);
// Focus first focusable element in new content
const firstFocusable = findFirstFocusableNode(newContent);
if (firstFocusable) {
// Announce to screen readers
firstFocusable.setAttribute("aria-live", "polite");
firstFocusable.setAttribute("aria-atomic", "true");
// Focus with slight delay to ensure DOM is updated
setTimeout(() => {
firstFocusable.focus();
}, 100);
}
}Accessibility Notes: