/** * Shared (free-eligible) WAI-ARIA radio-group helpers for the swatch renderer. * * Lives OUTSIDE src/frontend/extension/ so BOTH the free SwatchGroup and the Pro * ExtensionSwatchGroup import the SAME logic and render an identical a11y tree. * Pure/DOM-reading functions only — no React, no extension imports. * * Implements the WAI-ARIA Radio Group pattern: one tab stop per group (roving * tabindex), Arrow keys move + select, Home/End jump, Space/Enter select. * Disabled swatches never enter the roving order. Out-of-stock swatches remain * arrow-reachable only while the current settings leave them interactive; * hidden or explicitly disabled OOS swatches are excluded as well. */ import { isRtlElement, resolveRovingIndex, rovingTabIndex, } from "./rovingFocus"; export interface RovingContext { /** Values disabled by the native WooCommerce option state. */ disabledValues?: Iterable; /** Values considered out of stock for the current variation set. */ outOfStockValues?: Iterable; /** Single-product OOS behavior: "hide" removes options from the order. */ outOfStockBehavior?: string; /** Whether visible out-of-stock options are disabled. */ outOfStockDisable?: boolean; } export interface RadioKeyResult { /** The value whose radio should become focused/tabbable. */ targetValue: string; /** Whether this key expresses an intent to select (caller still gates on disabled). */ selectIntent: boolean; } function toSet(values: Iterable | undefined): Set { if (!values) return new Set(); return values instanceof Set ? values : new Set(values); } /** * Turn an attribute slug into a human label as a last-resort accessible name. * "attribute_pa_color" -> "Color", "attribute_size" -> "Size". */ export function deriveAttributeLabel(name: string): string { let slug = String(name || "").trim(); slug = slug.replace(/^attribute_/i, "").replace(/^pa_/i, ""); slug = slug.replace(/[_-]+/g, " ").trim(); if (!slug) return ""; return slug.replace(/\b\w/g, (c) => c.toUpperCase()); } function findLabelFor(doc: Document, id: string): Element | null { const view = doc.defaultView as (Window & typeof globalThis) | null; const escape = view?.CSS?.escape; if (typeof escape === "function") { return doc.querySelector(`label[for="${escape(id)}"]`); } const labels = doc.querySelectorAll("label[for]"); for (const label of Array.from(labels)) { if (label.getAttribute("for") === id) { return label; } } return null; } /** * Resolve the radiogroup's accessible name from the native select WooCommerce * renders: its