Flatten workspace (#120)

This commit is contained in:
Rahim
2025-10-23 15:19:36 -07:00
committed by GitHub
parent e41f883aea
commit 1ad0bc436f
215 changed files with 804 additions and 6445 deletions
+28
View File
@@ -0,0 +1,28 @@
/**
* Converts a `NamedNodeMap` to a plain object.
*/
export function namedNodeMapToObject(namedNodeMap: NamedNodeMap): Record<string, string> {
const obj: Record<string, string> = {};
for (const attr of namedNodeMap) {
obj[attr.name] = attr.value;
}
return obj;
}
/**
* Sets multiple attributes on an element and handles boolean attributes appropriately.
*
* @param element - The element to set attributes on.
* @param attributes - The attributes to set.
*/
export function setAttributes(element: HTMLElement, attributes: Record<string, string>): void {
for (const [key, value] of Object.entries(attributes)) {
if (typeof value === 'boolean') {
element.toggleAttribute(key, value);
} else {
element.setAttribute(key, value);
}
}
}
+20
View File
@@ -0,0 +1,20 @@
/**
* Get the active element, accounting for Shadow DOM subtrees.
*
* @param root - The root node to search for the active element.
*/
export function activeElement(
root: Document = document,
): Element | null {
let element = root.activeElement;
while (element?.shadowRoot?.activeElement != null) {
element = element.shadowRoot.activeElement;
}
return element;
}
export function getDocument(node: Element | null): Document {
return node?.ownerDocument ?? document;
}
+5
View File
@@ -0,0 +1,5 @@
export function isOutsideEvent(event: FocusEvent, container?: Element): boolean {
const containerElement = container || (event.currentTarget as Element);
const relatedTarget = event.relatedTarget as HTMLElement | null;
return !relatedTarget || !containerElement.contains(relatedTarget);
}
+5
View File
@@ -0,0 +1,5 @@
export * from './attributes';
export * from './element';
export * from './event';
export * from './keyboard';
export * from './shadow-dom';
+46
View File
@@ -0,0 +1,46 @@
import type { FocusableElement } from 'tabbable';
import { tabbable } from 'tabbable';
import { activeElement, getDocument } from './element';
export function getTabbableOptions(): Readonly<{
getShadowRoot: boolean;
displayCheck: 'full' | 'none';
}> {
// JSDOM does not support the `tabbable` library. To solve this we can
// check if `ResizeObserver` is a real function (not polyfilled), which
// determines if the current environment is JSDOM-like.
const isNativeResizeObserver: boolean = typeof ResizeObserver === 'function' && ResizeObserver.toString().includes('[native code]');
const displayCheck: 'full' | 'none' = isNativeResizeObserver ? 'full' : 'none';
return ({
getShadowRoot: true,
displayCheck: displayCheck as 'full' | 'none',
}) as const;
}
function getTabbableIn(container: HTMLElement, dir: 1 | -1): FocusableElement | undefined {
const list = tabbable(container, getTabbableOptions());
const len = list.length;
if (len === 0) {
return undefined;
}
const active = activeElement(getDocument(container)) as FocusableElement;
const index = list.indexOf(active);
const nextIndex = index === -1 ? (dir === 1 ? 0 : len - 1) : index + dir;
return list[nextIndex];
}
export function getNextTabbable(referenceElement: Element | null): FocusableElement | null {
return (
getTabbableIn(getDocument(referenceElement).body, 1) || (referenceElement as FocusableElement)
);
}
export function getPreviousTabbable(referenceElement: Element | null): FocusableElement | null {
return (
getTabbableIn(getDocument(referenceElement).body, -1) || (referenceElement as FocusableElement)
);
}
+12
View File
@@ -0,0 +1,12 @@
/**
* Utility function to check if a root node contains a child node across shadow DOM boundaries.
*/
export function containsComposedNode(rootNode: Node, childNode: Node): boolean {
if (!rootNode || !childNode) return false;
if (rootNode?.contains(childNode)) return true;
const childRootNode = childNode.getRootNode();
if (childRootNode && 'host' in childRootNode && childRootNode.host) {
return containsComposedNode(rootNode, childRootNode.host as Node);
}
return false;
}