Skip to content

<select> throws when a child contributes no variants (<setup>, <sort>, <collect>) #1875

Description

@dqnykamp

<select> fails to build when one of its children contributes no variants — <setup>, <sort> or <collect> alongside an <option> throws Cannot read properties of undefined (reading 'map'), so the whole document fails to render.

Reproduce

<select>
  <setup><number name="n">5</number></setup>
  <option><math>1</math></option>
</select>

Also reproduces with <sort> or <collect> in place of <setup>, in either order, with or without numToSelect. It does not reproduce with <conditionalContent>, <group>, <repeat>, <repeatForSequence>, <select>, <module> or <shuffle> in the same position — those build (some with an Invalid children for <select> warning, which is the expected complaint about a non-<option> child).

Cause

Select.determineNumberOfUniqueVariants builds numVariantsByChild from gatherVariantComponents(serializedComponent.children) — one entry per variant-bearing child. <setup>, <sort> and <collect> contribute none, so that array is shorter than the child list.

Select.getUniqueVariant then counts the children instead:

let numChildren = serializedComponent.children.length;   // Select.js:897
let childrenToSelect = serializedComponent.children;     // Select.js:898

With one <option> and one <setup>, numChildren is 2 while numVariantsByChild has length 1. The combination for index 1 reads numVariantsByChild[1] — undefined — so numberOfPossibilities becomes NaN. Every comparison against NaN is false, so the variantIndexLeft < chunksize * nCombinationsLeft guard falls to the else branch and the numberOfPossibilities > possibilitiesUsed filter drops every combination. The while loop exits with combinationIndexSelected still undefined, and combinations[undefined].map(...) throws at Select.js:986.

Candidate fix

Counting the variant-bearing children rather than all children removes all three failures:

let childrenToSelect =
    serializedComponent.variants.descendantVariantComponents ??
    serializedComponent.children;
let numChildren = numVariantsByChild.length;

With that change all three shapes build, and <select> with a <setup> sibling is clean (no warnings at all). src/test/variants/ and src/test/tagSpecific/select.test.ts — 103 tests — still pass. It has not been verified that variant selection is unchanged for existing documents, which is the part that needs care: the indices this function returns drive which option a seeded activity shows. A narrower alternative that provably cannot change any document that works today is to bail out instead of crashing:

if (numVariantsByChild.length < numChildren) {
    return { success: false };
}

Any document in that state throws today, so nothing that currently renders takes the new branch; the select falls back to non-unique variants.

Why now

#1872 marks <setup>, <sort> and <collect> allowInSchemaAnywhere, which makes them legal children of <select> in the schema. The runtime failure is older than that PR — an author could always write the markup — but the editor used to flag it and now does not. Swept over every (container, composite) pair that PR makes newly legal, with a legal sibling child present (1520 shapes), these three are the only ones that fail to build.

🤖 Generated with Claude Code

https://claude.ai/code/session_01P3ayJn1UrbSMKdAwQYkDGw

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Effort

    None yet

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions