const VARIATION_RESOLUTION_TIMEOUT_MS = 2000; const VARIATION_POLL_INTERVAL_MS = 25; function selectNamedAttribute( form: HTMLFormElement, name: string ): HTMLSelectElement | null { return Array.from( form.querySelectorAll( 'select' ) ) .find( ( select ) => select.name === name ) ?? null; } function resolvedCartButton( form: HTMLFormElement, variantId: string ): HTMLButtonElement | null { const variationInput = form.querySelector( 'input.variation_id, input[name="variation_id"]' ); const button = form.querySelector( 'button.single_add_to_cart_button' ); if ( variationInput?.value !== variantId || ! button || button.disabled || button.classList.contains( 'disabled' ) || button.getAttribute( 'aria-disabled' ) === 'true' ) { return null; } return button; } async function waitForResolvedCartButton( form: HTMLFormElement, variantId: string ): Promise { const deadline = Date.now() + VARIATION_RESOLUTION_TIMEOUT_MS; while ( Date.now() <= deadline ) { const button = resolvedCartButton( form, variantId ); if ( button ) return button; await new Promise( ( resolve ) => { window.setTimeout( resolve, VARIATION_POLL_INTERVAL_MS ); } ); } return null; } /** * Ask the live WooCommerce variation form to select a recommended variation, * then click add-to-cart only after the theme/Woo variation handler confirms * that exact id and enables the button. * * Themes and variation-swatch plugins commonly mirror custom controls into the * canonical selects asynchronously. We therefore mutate only those canonical * selects, emit their public `change` contract, and wait for Woo's own resolved * state instead of writing `variation_id` ourselves. */ export async function addRecommendedVariationToCart( variantId: string, selection: Record | undefined, doc: Document = document ): Promise { const form = doc.querySelector( 'form.variations_form' ); if ( ! form || ! selection || Object.keys( selection ).length === 0 ) return false; const changes: Array<{ select: HTMLSelectElement; value: string }> = []; for ( const [ name, value ] of Object.entries( selection ) ) { const select = selectNamedAttribute( form, name ); if ( ! select ) return false; select.value = value; if ( select.value !== value ) return false; changes.push( { select, value } ); } // Set the full combination first. Every subsequent change event then exposes // a complete selection to WooCommerce and to swatch/theme adapters. for ( const { select } of changes ) { select.dispatchEvent( new Event( 'change', { bubbles: true } ) ); } const button = await waitForResolvedCartButton( form, variantId ); if ( ! button || ! form.isConnected ) return false; button.click(); return true; }