@@ -581,3 +581,82 @@ export const renderContent = (content = [], {
581581export const getRandomId = ( prefix = "spectrum" ) => {
582582 return `${ prefix } -${ Math . random ( ) . toString ( 36 ) . substring ( 2 , 7 ) } ` ;
583583} ;
584+
585+ /**
586+ * For a given array of test or state descriptor objects, generate all non-empty combinations of the provided items.
587+ *
588+ * rules and behavior:
589+ * - each input item must include a unique `testHeading` string.
590+ * - items may include `not: string[]` where each string is a `testHeading` that
591+ * cannot co-exist with the item in the same combination.
592+ * - combinations that include any forbidden pairs (as declared via `not`) are
593+ * excluded from the output.
594+ * - output objects merge all boolean flags from members of the combination
595+ * (e.g., `isChecked`, `isDisabled`, etc.).
596+ * - the `not` property is never merged into the output; only behavioral flags
597+ * and the concatenated `testHeading` are emitted.
598+ *
599+ * implementation notes:
600+ * - combinations are enumerated via a bitmask from 1..(2^n - 1), thereby
601+ * excluding the empty set by design.
602+ * - constraint checking is done using the human-readable `testHeading` values
603+ * so authors can declare `not: ["Heading A", "Heading B"]` without having
604+ * to repeat flag keys.
605+ *
606+ * @typedef {Object } StateItem
607+ * @property {string } testHeading human-readable name for the state.
608+ * @property {string[] } [not] list of `testHeading` values that cannot co-exist.
609+ * @property {boolean } [isChecked]
610+ * @property {boolean } [isIndeterminate]
611+ * @property {boolean } [isEmphasized]
612+ * @property {boolean } [isReadOnly]
613+ * @property {boolean } [isDisabled]
614+ * @property {boolean } [isHovered]
615+ * @property {boolean } [isActive]
616+ * @property {boolean } [isFocused]
617+ *
618+ * @param {StateItem[] } items array of state descriptor objects.
619+ * @returns {Array<Record<string, unknown>> } all valid merged combinations.
620+ */
621+ export const getAllCombinations = ( items ) => {
622+ // store all valid combinations
623+ const combos = [ ] ;
624+ const count = items . length ;
625+
626+ // use a bitmask to generate the power set (excluding empty set)
627+ for ( let mask = 1 ; mask < ( 1 << count ) ; mask ++ ) {
628+ // materialize the current combination from selected bits
629+ const combo = [ ] ;
630+ for ( let i = 0 ; i < count ; i ++ ) {
631+ if ( mask & ( 1 << i ) ) combo . push ( items [ i ] ) ;
632+ }
633+
634+ // build a set of headings present in this combination for fast lookup
635+ const headings = new Set ( combo . map ( ( s ) => s . testHeading ) ) ;
636+
637+ // if any item declares a `not` array that includes any other heading in the
638+ // combination, then the combination is invalid and must be skipped
639+ const hasForbiddenPair = combo . some ( ( item ) => {
640+ const blocked = Array . isArray ( item . not ) ? item . not : [ ] ;
641+ return blocked . some ( ( h ) => headings . has ( h ) ) ;
642+ } ) ;
643+ if ( hasForbiddenPair ) continue ;
644+
645+ // merge flags from items in the combination, skipping meta fields
646+ const flags = combo . reduce ( ( acc , cur ) => {
647+ for ( const [ key , value ] of Object . entries ( cur ) ) {
648+ if ( key === "testHeading" || key === "not" ) continue ;
649+ acc [ key ] = value ;
650+ }
651+ return acc ;
652+ } , { } ) ;
653+
654+ // emit merged flags and a concatenated, readable heading
655+ combos . push ( {
656+ ...flags ,
657+ testHeading : combo . map ( ( s ) => s . testHeading ) . join ( " + " ) ,
658+ } ) ;
659+ }
660+
661+ return combos ;
662+ } ;
0 commit comments