@vectoriox/iox-ui 5.39.0 → 5.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/composite-core/index.d.ts +210 -12
  2. package/design-core/index.d.ts +694 -17
  3. package/fesm2022/vectoriox-iox-ui-actions-core.mjs +4 -3
  4. package/fesm2022/vectoriox-iox-ui-actions-core.mjs.map +1 -1
  5. package/fesm2022/vectoriox-iox-ui-composite-core.mjs +535 -121
  6. package/fesm2022/vectoriox-iox-ui-composite-core.mjs.map +1 -1
  7. package/fesm2022/vectoriox-iox-ui-design-core.mjs +843 -45
  8. package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
  9. package/fesm2022/vectoriox-iox-ui-generation-core.mjs +894 -21
  10. package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
  11. package/fesm2022/vectoriox-iox-ui.mjs +3945 -3914
  12. package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
  13. package/generation-core/index.d.ts +625 -5
  14. package/index.d.ts +22 -1
  15. package/package.json +1 -1
  16. package/src/actions-core/element-actions.ts +4 -3
  17. package/src/composite-core/accordion.ts +1 -0
  18. package/src/composite-core/composite-kinds.ts +42 -6
  19. package/src/composite-core/exclusive-runtime.ts +60 -10
  20. package/src/composite-core/exclusive.ts +64 -0
  21. package/src/composite-core/public-api.ts +5 -2
  22. package/src/composite-core/segmented.ts +349 -0
  23. package/src/composite-core/structure.ts +6 -1
  24. package/src/composite-core/tabs.ts +6 -3
  25. package/src/design-core/contrast-audit.ts +8 -3
  26. package/src/design-core/flavor-spec.generated.ts +832 -41
  27. package/src/generation-core/compose-site.ts +248 -12
  28. package/src/generation-core/nav-shape.ts +53 -3
  29. package/src/generation-core/node-baselines.generated.ts +49 -0
  30. package/src/generation-core/patterns.ts +20 -0
  31. package/src/generation-core/site-spec.generated.ts +595 -0
  32. package/styles/iox-design.css +19 -0
package/index.d.ts CHANGED
@@ -1726,6 +1726,16 @@ declare const ON_STATE_ROLES: ReadonlyArray<string>;
1726
1726
  * the same Item open state with a picked trigger.
1727
1727
  */
1728
1728
  declare function componentStatesFor(type: string | null | undefined, role?: string | null, insideItem?: boolean, itemsToPick?: boolean): ComponentState[];
1729
+ /**
1730
+ * The states a node's rules are emitted LAST in (CC4b): `on` (`[data-iox-on]`) and `focus` (`:focus-visible`) tie on
1731
+ * specificity, so whichever comes later wins — a focused Segmented option that is ON must keep its focus ring over the
1732
+ * on shadow (board 10). Every renderer emits a node's state rules in `orderStatesForEmit` order.
1733
+ */
1734
+ declare const LAST_EMITTED_STATES: ReadonlyArray<string>;
1735
+ /** `states` in the order their rules are emitted: as given, with the LAST_EMITTED_STATES moved to the end. */
1736
+ declare function orderStatesForEmit<T extends string>(states: ReadonlyArray<T>): T[];
1737
+ /** True when a rule key (`<nodeId>:<state>`) holds a state emitted last. */
1738
+ declare function isLastEmittedStateKey(key: string): boolean;
1729
1739
  /**
1730
1740
  * What follows `.iox-node-<id>` in a stored state's rule: `:hover`, `:focus-visible`, or a component
1731
1741
  * state's attribute selector (`[aria-current="page"]`). Shared by the builder canvas and both engine paths.
@@ -2044,6 +2054,9 @@ declare const CURRENT_LINK_TYPES: ReadonlyArray<string>;
2044
2054
  /** The types that are pure decoration to assistive technology (an Icon has no name of its own). */
2045
2055
  declare const DECORATIVE_TYPES: ReadonlyArray<string>;
2046
2056
  declare const DIALOG_NODE_TYPE = "Dialog";
2057
+ /** A Segmented control's root (CC4b) is a radio group that must be named (board 10: an empty name is an audit
2058
+ * finding). The main entry does not import composite-core, so the role is spelled here — a11y spec pins it. */
2059
+ declare const NAMED_GROUP_ROLES: ReadonlyArray<string>;
2047
2060
  /** Carries the min-width (px) at which an open dialog closes itself; the engine listens on it. */
2048
2061
  declare const DIALOG_CLOSE_ABOVE_ATTRIBUTE = "data-iox-dialog-close-above";
2049
2062
  declare enum A11yAttribute {
@@ -2100,6 +2113,14 @@ declare function syncNodeA11yAttributes(el: {
2100
2113
  declare function needsAccessibleName(node: (A11yNodeSource & {
2101
2114
  children?: ReadonlyArray<unknown> | null;
2102
2115
  }) | null | undefined): boolean;
2116
+ /**
2117
+ * True for a GROUP that must be named and is not — a Segmented control's radio group (CC4b, board 10): a screen reader
2118
+ * says "radio group" and nothing about what is being chosen. The responsive audit flags it; named in Bindings ▸
2119
+ * Accessibility.
2120
+ */
2121
+ declare function needsGroupName(node: (A11yNodeSource & {
2122
+ role?: string | null;
2123
+ }) | null | undefined): boolean;
2103
2124
 
2104
2125
  /**
2105
2126
  * Reduced motion (CC1, R1 — complex-components plan §2d). The token sheet already collapses the
@@ -3404,5 +3425,5 @@ declare class GalleryLoop {
3404
3425
  private emit;
3405
3426
  }
3406
3427
 
3407
- export { A11yAttribute, A11yTrait, ACCESSIBLE_NAME_TYPES, AOSService, ARIA_CURRENT_PAGE, BACKDROP_TWIN_KEY, BACKDROP_WHOLE_KEY, BREAKPOINTS, Breakpoint, BreakpointQueryKind, BuilderButtonComponent, BuilderDividerComponent, BuilderHeadingComponent, BuilderIconComponent, BuilderImageComponent, BuilderLinkComponent, BuilderSpacerComponent, ButtonBlockComponent, COMPONENT_STATE_SELECTOR, CONTAINER_HTML_TAGS, CONTEXT_ACTION_TYPES, CURRENT_LINK_TYPES, CardComponent, ClientGalleryComponent, ClientListComponent, ClientListItemComponent, ClientRepeaterComponent, ClientSliderContainerComponent, ClientSliderSlideComponent, ComponentInstanceRegistryService, ComponentState, ContainerComponent, ContextStore, DECORATIVE_TYPES, DEFAULT_GALLERY_SCHEDULER, DEFAULT_PAGE_TRANSITION, DEFAULT_PAGE_TRANSITION_DURATION, DEFAULT_PAGE_TRANSITION_EASING, DEFAULT_PAGE_TRANSITION_PERSPECTIVE, DEFAULT_PHOSPHOR_WEIGHT, DEFAULT_WIDTH_BY_TYPE, DIALOG_CLOSE_ABOVE_ATTRIBUTE, DIALOG_NODE_TYPE, DataSourceSortDir, DeclarationContext, ENTRANCE_ACTION_TYPES, EXIT_ACTION_TYPES, FormRenderState, FormRuntimeService, GALLERY_EFFECTS, GALLERY_EFFECT_DESCRIPTORS, GalleryLoop, HOVER_DEFAULT_TRANSITION, HOVER_STATES, INTERACTION_HIDDEN_ATTRIBUTE, IOX_BUILDER_EVENTS, IOX_FORM_MESSAGES, IOX_FORM_NAVIGATOR, IOX_FORM_TRANSPORT, IS_PREVIEW, IoxAnimateContainer, IoxAosDirective, IoxAosModule, IoxBuilderComponentsModule, IoxComponentRegistryService, IoxDialogComponent, IoxFieldComponent, IoxFormComponent, IoxFormErrorComponent, IoxFormSuccessComponent, IoxPageComponent, IoxPageModule, IoxSubmitButtonComponent, IoxUiModule, IoxVideoComponent, LEGACY_DIRECTION_KEY, LEGACY_HOVER_TRANSITION, LEGACY_SWIPER_CONTROLS, LinkedContainerComponent, MANAGED_A11Y_ATTRIBUTES, MAX_LINES_KEY, ON_STATE_ROLES, PAGE_STYLE_TOKENS, PAGE_TOKEN_FALLBACKS, PAGE_TRANSITION_CATEGORIES, PAGE_TRANSITION_PRESETS, PART_CLASS_NAMES, PART_PAGINATION_MODIFIER, PIPE_DESCRIPTORS, PIPE_REGISTRY, PageScrollProvider, PageStyleField, PageStyleSource, REDUCED_MOTION_QUERY, RESERVED_ALIAS_PREFIX, SLIDER_INPUTS_ATTRIBUTE, SLIDER_INPUT_NAMES, SORT_PARAM, STATE_PSEUDO_CLASS, SUPPRESSED_STYLE_PROPS_BY_TYPE, SWIPER_A11Y_OPTIONS, SWIPER_RESPONSIVE_TRAITS, SWIPER_TRIGGER_EVENTS, SectionComponent, SwiperModuleName, TARGETED_STATE_SUFFIX, TEXT_ALIGN_KEY, TEXT_DIRECTION_KEY, TRANSITION_FEELS, TextAlign, TextBlockComponent, VALIDATION_STATES, VALIDATION_STATE_CLASS, VIRTUAL_TRAIT_GROUPS, VIRTUAL_TRAIT_KEYS, ViewPositionDirective, ViewPositionModule, VisibilityActionType, activeActions, applyBindingPipes, applyFeel, bootSlider, breakpointDef, breakpointForWidth, breakpointQueryFor, buildDataSourceFilter, buildInteractionKeyframes, buildPartSelector, buildRelatedFilter, buildSwiperBreakpoints, buildTargetedStateSelector, buildTargetedSuffixSelector, buildTransitionTimeline, cascadeChain, clearInputOverride, clearOverride, closeAboveMediaQuery, collectContextSlotKeys, collectReferencedAliases, compileDeclarations, compilePageStyle, componentStatesFor, composeVirtualTraits, computeRelative, containerHtmlTag, contextActionEffect, contextSlotKeys, countOverrides, dataSourcePlanToRequest, declaredInputNames, decomposeBackdropFilter, defaultHoverTransition, defaultMessageResolver, deriveSeoFromContent, describePageStyle, describeSeoFields, dialogCloseAboveMinWidth, domHasSliderParts, effectiveActionType, effectiveTransitionId, entrancePreStateStyle, excludeSelf, feelFor, finalKeyframe, getByPath, getSeoValue, hasHoverState, hasOwnTransition, initialSlotValues, isActionEnabled, isContextAction, isCurrentLink, isExternalOrFragment, isGradientPaint, isInputOverriddenAt, isOverriddenAt, isReservedAlias, isValidSlotKey, isWholeBackdropSet, keyframePropertyName, keyframeStyleEntries, legacyEnterToTransition, matchRouteParams, mediaQueryFor, mergeSwiperBreakpoints, migrateLegacyDirection, migrateLegacyTextAlign, motionAllowed, nearestCommonAncestorId, needsAccessibleName, nodeA11yAttributes, nodeCssId, nodeTraitValue, nodeUsesContext, normalizeAppPath, normalizeCollectionResult, normalizeGalleryImage, normalizeIconClass, parentBreakpoint, parseUrlList, partClassName, planDataSource, planResponsiveRules, planSwiperOptions, prefersReducedMotion, referencedDataSources, referencedDataSourcesDeep, renderDefaultDeclarations, resetPageStyleField, resetSeoField, resolveBoundValue, resolveContextValue, resolveDerivedInto, resolveGalleryEffect, resolveGalleryUrls, resolveInputChain, resolveInputs, resolvePageTransition, resolvePath, resolveRelativeInto, resolveRepeaterItems, resolveSeo, resolveSiblings, resolveSingleItemId, resolveStateStyles, resolveStyleProps, resolveToggleVisibility, resolveValueChain, rewriteViewportUnits, safeSetInput, serializeA11yAttributes, setInputOverride, setOverride, setSeoAuto, setSeoBinding, setSeoOverride, setSeoValue, sliderControlElements, sliderInputsFromProps, sliderInputsOf, sortParamValue, stateNamesIn, statePseudoClass, stateSelectorSuffix, stripSuppressedProps, swiperAutoplayAllowed, swiperLoopOptions, swiperModuleNames, syncNodeA11yAttributes, targetedStateSelector, variableAliases, variableItemsFrom, withContextSlots, withDefaultHoverTransition, wrapInBreakpointQuery, wrapInMediaQuery };
3428
+ export { A11yAttribute, A11yTrait, ACCESSIBLE_NAME_TYPES, AOSService, ARIA_CURRENT_PAGE, BACKDROP_TWIN_KEY, BACKDROP_WHOLE_KEY, BREAKPOINTS, Breakpoint, BreakpointQueryKind, BuilderButtonComponent, BuilderDividerComponent, BuilderHeadingComponent, BuilderIconComponent, BuilderImageComponent, BuilderLinkComponent, BuilderSpacerComponent, ButtonBlockComponent, COMPONENT_STATE_SELECTOR, CONTAINER_HTML_TAGS, CONTEXT_ACTION_TYPES, CURRENT_LINK_TYPES, CardComponent, ClientGalleryComponent, ClientListComponent, ClientListItemComponent, ClientRepeaterComponent, ClientSliderContainerComponent, ClientSliderSlideComponent, ComponentInstanceRegistryService, ComponentState, ContainerComponent, ContextStore, DECORATIVE_TYPES, DEFAULT_GALLERY_SCHEDULER, DEFAULT_PAGE_TRANSITION, DEFAULT_PAGE_TRANSITION_DURATION, DEFAULT_PAGE_TRANSITION_EASING, DEFAULT_PAGE_TRANSITION_PERSPECTIVE, DEFAULT_PHOSPHOR_WEIGHT, DEFAULT_WIDTH_BY_TYPE, DIALOG_CLOSE_ABOVE_ATTRIBUTE, DIALOG_NODE_TYPE, DataSourceSortDir, DeclarationContext, ENTRANCE_ACTION_TYPES, EXIT_ACTION_TYPES, FormRenderState, FormRuntimeService, GALLERY_EFFECTS, GALLERY_EFFECT_DESCRIPTORS, GalleryLoop, HOVER_DEFAULT_TRANSITION, HOVER_STATES, INTERACTION_HIDDEN_ATTRIBUTE, IOX_BUILDER_EVENTS, IOX_FORM_MESSAGES, IOX_FORM_NAVIGATOR, IOX_FORM_TRANSPORT, IS_PREVIEW, IoxAnimateContainer, IoxAosDirective, IoxAosModule, IoxBuilderComponentsModule, IoxComponentRegistryService, IoxDialogComponent, IoxFieldComponent, IoxFormComponent, IoxFormErrorComponent, IoxFormSuccessComponent, IoxPageComponent, IoxPageModule, IoxSubmitButtonComponent, IoxUiModule, IoxVideoComponent, LAST_EMITTED_STATES, LEGACY_DIRECTION_KEY, LEGACY_HOVER_TRANSITION, LEGACY_SWIPER_CONTROLS, LinkedContainerComponent, MANAGED_A11Y_ATTRIBUTES, MAX_LINES_KEY, NAMED_GROUP_ROLES, ON_STATE_ROLES, PAGE_STYLE_TOKENS, PAGE_TOKEN_FALLBACKS, PAGE_TRANSITION_CATEGORIES, PAGE_TRANSITION_PRESETS, PART_CLASS_NAMES, PART_PAGINATION_MODIFIER, PIPE_DESCRIPTORS, PIPE_REGISTRY, PageScrollProvider, PageStyleField, PageStyleSource, REDUCED_MOTION_QUERY, RESERVED_ALIAS_PREFIX, SLIDER_INPUTS_ATTRIBUTE, SLIDER_INPUT_NAMES, SORT_PARAM, STATE_PSEUDO_CLASS, SUPPRESSED_STYLE_PROPS_BY_TYPE, SWIPER_A11Y_OPTIONS, SWIPER_RESPONSIVE_TRAITS, SWIPER_TRIGGER_EVENTS, SectionComponent, SwiperModuleName, TARGETED_STATE_SUFFIX, TEXT_ALIGN_KEY, TEXT_DIRECTION_KEY, TRANSITION_FEELS, TextAlign, TextBlockComponent, VALIDATION_STATES, VALIDATION_STATE_CLASS, VIRTUAL_TRAIT_GROUPS, VIRTUAL_TRAIT_KEYS, ViewPositionDirective, ViewPositionModule, VisibilityActionType, activeActions, applyBindingPipes, applyFeel, bootSlider, breakpointDef, breakpointForWidth, breakpointQueryFor, buildDataSourceFilter, buildInteractionKeyframes, buildPartSelector, buildRelatedFilter, buildSwiperBreakpoints, buildTargetedStateSelector, buildTargetedSuffixSelector, buildTransitionTimeline, cascadeChain, clearInputOverride, clearOverride, closeAboveMediaQuery, collectContextSlotKeys, collectReferencedAliases, compileDeclarations, compilePageStyle, componentStatesFor, composeVirtualTraits, computeRelative, containerHtmlTag, contextActionEffect, contextSlotKeys, countOverrides, dataSourcePlanToRequest, declaredInputNames, decomposeBackdropFilter, defaultHoverTransition, defaultMessageResolver, deriveSeoFromContent, describePageStyle, describeSeoFields, dialogCloseAboveMinWidth, domHasSliderParts, effectiveActionType, effectiveTransitionId, entrancePreStateStyle, excludeSelf, feelFor, finalKeyframe, getByPath, getSeoValue, hasHoverState, hasOwnTransition, initialSlotValues, isActionEnabled, isContextAction, isCurrentLink, isExternalOrFragment, isGradientPaint, isInputOverriddenAt, isLastEmittedStateKey, isOverriddenAt, isReservedAlias, isValidSlotKey, isWholeBackdropSet, keyframePropertyName, keyframeStyleEntries, legacyEnterToTransition, matchRouteParams, mediaQueryFor, mergeSwiperBreakpoints, migrateLegacyDirection, migrateLegacyTextAlign, motionAllowed, nearestCommonAncestorId, needsAccessibleName, needsGroupName, nodeA11yAttributes, nodeCssId, nodeTraitValue, nodeUsesContext, normalizeAppPath, normalizeCollectionResult, normalizeGalleryImage, normalizeIconClass, orderStatesForEmit, parentBreakpoint, parseUrlList, partClassName, planDataSource, planResponsiveRules, planSwiperOptions, prefersReducedMotion, referencedDataSources, referencedDataSourcesDeep, renderDefaultDeclarations, resetPageStyleField, resetSeoField, resolveBoundValue, resolveContextValue, resolveDerivedInto, resolveGalleryEffect, resolveGalleryUrls, resolveInputChain, resolveInputs, resolvePageTransition, resolvePath, resolveRelativeInto, resolveRepeaterItems, resolveSeo, resolveSiblings, resolveSingleItemId, resolveStateStyles, resolveStyleProps, resolveToggleVisibility, resolveValueChain, rewriteViewportUnits, safeSetInput, serializeA11yAttributes, setInputOverride, setOverride, setSeoAuto, setSeoBinding, setSeoOverride, setSeoValue, sliderControlElements, sliderInputsFromProps, sliderInputsOf, sortParamValue, stateNamesIn, statePseudoClass, stateSelectorSuffix, stripSuppressedProps, swiperAutoplayAllowed, swiperLoopOptions, swiperModuleNames, syncNodeA11yAttributes, targetedStateSelector, variableAliases, variableItemsFrom, withContextSlots, withDefaultHoverTransition, wrapInBreakpointQuery, wrapInMediaQuery };
3408
3429
  export type { A11yNodeSource, BoundValueSpec, BreakpointDef, BuildRelatedFilterOptions, ContextActionEffect, ContextActionLike, ContextItemRef, ContextSlotDefault, ContextValueSpec, ContextWriteScope, CurrentLinkOptions, DataSourceDomain, DataSourceEndpoints, DataSourceMode, DataSourcePlan, DataSourceQueryFilter, DataSourceRequest, DataSourceRequestSpec, DataSourceRouteFilter, DataSourceSort, DataSourceSpec, FormMessageResolver, GalleryBoundSource, GalleryDurations, GalleryEffect, GalleryEffectDescriptor, GalleryImage, GalleryLoopOptions, GalleryLoopScheduler, GalleryLoopState, GallerySlideState, IBuilderEventEmitter, InputMap, InteractionKeyframe, IoxContextSlot, IoxPipe, MatchMediaHost, NodeA11yContext, OverrideBreakpoint, PageStyleFieldState, PageStyleOptions, PageStyleSettings, PageTransitionCategory, PageTransitionCategoryMeta, PageTransitionOptions, PageTransitionPreset, PipeArgSpec, PipeContext, PipeDescriptor, RelativeInputs, RelativeSourceKind, RelativeSourceSpec, ResolveSiblingsOptions, ResolvedPageTransition, ResponsiveLayer, ResponsiveOverrides, ResponsiveRuleSet, RouteContext, SeoDerivedContent, SeoFieldSource, SeoFieldState, SeoResolveContext, SetContextParams, SiblingsResult, SliderBootOptions, SliderControlElements, SliderInputs, StateStyleMap, StyleMap, StyleTreeNode, SwiperBreakpointMap, SwiperModuleFlags, SwiperPlan, SwiperPlanContext, ToggleableAction, TransitionFeel, TransitionPhase, TransitionTimelineInput, ValueSource, VirtualTraitGroup };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectoriox/iox-ui",
3
- "version": "5.39.0",
3
+ "version": "5.41.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": ">=20.0.0",
6
6
  "@angular/core": ">=20.0.0",
@@ -1,11 +1,12 @@
1
- import { AccordionRole, CompositeItemAction, DisclosureRole, SLIDE_TO_ROW, TabsRole } from '@vectoriox/iox-ui/composite-core';
1
+ import { AccordionRole, CompositeItemAction, DisclosureRole, SegmentedRole, SLIDE_TO_ROW, TabsRole } from '@vectoriox/iox-ui/composite-core';
2
2
 
3
3
  import { ActionDescriptor } from './action.model';
4
4
 
5
5
  /** The items an item action may target: an accordion's item, or a disclosure (its own one item). */
6
6
  const ITEM_TARGET_ROLES = [AccordionRole.Item, DisclosureRole.Disclosure];
7
- /** Open also takes a TAB (CC4a): it turns it on. A Tabs always shows one, so Close and Toggle never aim at one. */
8
- const OPEN_TARGET_ROLES = [...ITEM_TARGET_ROLES, TabsRole.Trigger];
7
+ /** Open also takes a TAB (CC4a) and a Segmented control's OPTION (CC4b, "Turn on"): it turns it on. Both always show
8
+ * one, so Close and Toggle never aim at either. */
9
+ const OPEN_TARGET_ROLES = [...ITEM_TARGET_ROLES, TabsRole.Trigger, SegmentedRole.Option];
9
10
 
10
11
  /**
11
12
  * The ELEMENT action catalog (actions plan A1) — what a node can DO, as data. One list feeds the
@@ -481,6 +481,7 @@ const DOM_ID_PREFIX: Readonly<Record<CompositeKind, string>> = {
481
481
  [CompositeKind.Accordion]: 'iox-acc',
482
482
  [CompositeKind.Disclosure]: 'iox-dis',
483
483
  [CompositeKind.Tabs]: 'iox-tab',
484
+ [CompositeKind.Segmented]: 'iox-seg',
484
485
  };
485
486
 
486
487
  /** The ARIA pairing ids of an item: unique per composite (its SCOPE id — `compositeScopeId`) and per item (its key). */
@@ -2,8 +2,9 @@
2
2
  * The composites' kinds and roles. Two families:
3
3
  * - OPEN/CLOSE (CC2 Accordion, CC2.1 Disclosure — composite plan T9 / T21): a Disclosure is a one-item accordion whose
4
4
  * ROOT is its item, so both run the same state machine, attribute planners and runtime (`accordion-runtime.ts`);
5
- * - EXCLUSIVE (CC4 Tabs — composite T17; the Segmented control joins in CC4b): exactly one item is on, a trigger and
6
- * its panel paired by `itemKey` (`tabs.ts`, `exclusive-runtime.ts`).
5
+ * - EXCLUSIVE (CC4 Tabs — composite T17; CC4b the Segmented control — T32): exactly one item is on; a Tabs' trigger
6
+ * and its panel pair by `itemKey` (`tabs.ts`), a Segmented control's options have no panel (`segmented.ts`); both run
7
+ * `exclusive-runtime.ts`.
7
8
  * Every consumer that treats "an item", "a trigger" or "a panel" alike asks these predicates, never a role literal.
8
9
  */
9
10
 
@@ -11,12 +12,13 @@ export enum CompositeKind {
11
12
  Accordion = 'accordion',
12
13
  Disclosure = 'disclosure',
13
14
  Tabs = 'tabs',
15
+ Segmented = 'segmented',
14
16
  }
15
17
 
16
18
  /** The kinds the open/close runtime runs (`AccordionRuntime`). */
17
19
  export type ToggleCompositeKind = CompositeKind.Accordion | CompositeKind.Disclosure;
18
20
  /** The kinds the exclusive runtime runs (`ExclusiveRuntime`): one item on at a time, always one. */
19
- export type ExclusiveCompositeKind = CompositeKind.Tabs;
21
+ export type ExclusiveCompositeKind = CompositeKind.Tabs | CompositeKind.Segmented;
20
22
 
21
23
  export enum AccordionRole {
22
24
  Accordion = 'accordion',
@@ -53,6 +55,17 @@ export enum TabsRole {
53
55
  Panel = 'tabs.panel',
54
56
  }
55
57
 
58
+ /**
59
+ * SEGMENTED (CC4b — composite T32; complex-components board 10). One of a few, always one on, FIXED options (R3): the
60
+ * root IS the radio group and holds the options directly — no list, no panels. An option may hold the optional saving
61
+ * badge (inside it, so its words join the option's accessible name).
62
+ */
63
+ export enum SegmentedRole {
64
+ Segmented = 'segmented',
65
+ Option = 'segmented.option',
66
+ Badge = 'segmented.badge',
67
+ }
68
+
56
69
  interface RoleLike { role?: string | null }
57
70
 
58
71
  /** The roles each open/close kind plays in the shared runtime. */
@@ -69,11 +82,12 @@ export const COMPOSITE_KIND_ROLES: Readonly<Record<ToggleCompositeKind, Composit
69
82
  [CompositeKind.Disclosure]: { root: DisclosureRole.Disclosure, item: DisclosureRole.Disclosure, trigger: DisclosureRole.Trigger, panel: DisclosureRole.Panel, icon: DisclosureRole.Icon },
70
83
  };
71
84
 
72
- /** The roles each EXCLUSIVE kind plays: its root, the holder of its triggers, its triggers, and — when its items show
73
- * a panel (Tabs; a Segmented control has none) — the holder of its panels and the panels. */
85
+ /** The roles each EXCLUSIVE kind plays: its root, the holder of its triggers (null when the root holds them — a
86
+ * Segmented control), its triggers, and — when its items show a panel (Tabs; a Segmented control has none) — the
87
+ * holder of its panels and the panels. */
74
88
  export interface ExclusiveKindRoles {
75
89
  root: string;
76
- list: string;
90
+ list: string | null;
77
91
  trigger: string;
78
92
  panels: string | null;
79
93
  panel: string | null;
@@ -81,6 +95,7 @@ export interface ExclusiveKindRoles {
81
95
 
82
96
  export const EXCLUSIVE_KIND_ROLES: Readonly<Record<ExclusiveCompositeKind, ExclusiveKindRoles>> = {
83
97
  [CompositeKind.Tabs]: { root: TabsRole.Tabs, list: TabsRole.List, trigger: TabsRole.Trigger, panels: TabsRole.Panels, panel: TabsRole.Panel },
98
+ [CompositeKind.Segmented]: { root: SegmentedRole.Segmented, list: null, trigger: SegmentedRole.Option, panels: null, panel: null },
84
99
  };
85
100
 
86
101
  const KINDS = Object.values(CompositeKind);
@@ -200,6 +215,27 @@ export function isTabsPart(node: RoleLike | null | undefined): boolean {
200
215
  return compositeKindOfRole(node?.role) === CompositeKind.Tabs;
201
216
  }
202
217
 
218
+ /** True for any part of a Segmented control (its root included). */
219
+ export function isSegmentedPart(node: RoleLike | null | undefined): boolean {
220
+ return compositeKindOfRole(node?.role) === CompositeKind.Segmented;
221
+ }
222
+
223
+ /** The exclusive kind a node's role belongs to (`segmented.option` → Segmented), or null. */
224
+ export function exclusiveKindOf(node: RoleLike | null | undefined): ExclusiveCompositeKind | null {
225
+ const kind = compositeKindOfRole(node?.role);
226
+ return isExclusiveKind(kind) ? kind : null;
227
+ }
228
+
229
+ /** True when an exclusive root holds its triggers itself (a Segmented control), not in a list part. */
230
+ export function exclusiveRootHoldsTriggers(kind: ExclusiveCompositeKind): boolean {
231
+ return EXCLUSIVE_KIND_ROLES[kind].list === null;
232
+ }
233
+
234
+ /** True when an exclusive kind's items show a panel (Tabs), so a trigger and its panel are a pair. */
235
+ export function exclusiveKindHasPanels(kind: ExclusiveCompositeKind | null | undefined): boolean {
236
+ return !!kind && EXCLUSIVE_KIND_ROLES[kind].panel !== null;
237
+ }
238
+
203
239
  /** A root of either family — anything that runs a composite runtime. */
204
240
  export function isAnyCompositeRoot(node: RoleLike | null | undefined): boolean {
205
241
  return isCompositeRoot(node) || isExclusiveRoot(node);
@@ -1,15 +1,16 @@
1
1
  import { roleClass } from '@vectoriox/iox-ui/design-core';
2
2
 
3
- import { accordionDomIds, accordionFocusIndex, COMPOSITE_ATTRIBUTE, ITEM_KEY_ATTRIBUTE, ON_ATTRIBUTE, SCOPE_ATTRIBUTE } from './accordion';
3
+ import { accordionDomIds, accordionFocusIndex, COMPOSITE_ATTRIBUTE, DEFAULT_ON_ATTRIBUTE, ITEM_KEY_ATTRIBUTE, ON_ATTRIBUTE, SCOPE_ATTRIBUTE } from './accordion';
4
4
  import { ITEM_OFF_EVENT, ITEM_ON_EVENT } from './accordion-runtime';
5
- import { CompositeKind, EXCLUSIVE_KIND_ROLES, ExclusiveCompositeKind, isExclusiveKind } from './composite-kinds';
5
+ import { CompositeKind, EXCLUSIVE_KIND_ROLES, ExclusiveCompositeKind, exclusiveRootHoldsTriggers, isExclusiveKind } from './composite-kinds';
6
+ import { segmentedKeyAction } from './segmented';
6
7
  import {
7
8
  ACTIVATE_ON_ATTRIBUTE, ORIENTATION_ATTRIBUTE, TABS_ACTIVATE_ONS, TABS_ORIENTATIONS, TabsActivateOn, TabsBehaviour, tabsKeyAction, TabsOrientation,
8
9
  } from './tabs';
9
10
 
10
11
  /**
11
- * The EXCLUSIVE composites' RUNTIME (CC4a Tabs; the Segmented control runs it too in CC4b) — ONE copy, DOM only, for
12
- * the builder canvas and the live site (both engine paths render the same markup from `tabs.ts`'s attribute planners):
12
+ * The EXCLUSIVE composites' RUNTIME (CC4a Tabs; CC4b the Segmented control) — ONE copy, DOM only, for the builder canvas
13
+ * and the live site (both engine paths render the same markup from `tabs.ts`'s and `segmented.ts`'s attribute planners):
13
14
  *
14
15
  * [root] data-iox-composite="tabs" data-iox-activate-on data-iox-orientation [data-iox-scope]
15
16
  * .iox-role-tabs-list role=tablist [aria-orientation=vertical]
@@ -25,9 +26,20 @@ import {
25
26
  * `iox-item-off` on the trigger AND its panel (the `itemOn` / `itemOff` interaction triggers). The item shown is
26
27
  * FORCED ?? LIVE: the builder forces one (a selection inside an inactive panel); the live site forces nothing. The
27
28
  * server's first state is trusted (no flash).
29
+ *
30
+ * A SEGMENTED control (CC4b) is the same machine with no panels and the APG RADIO GROUP's markup and keys:
31
+ *
32
+ * [root] data-iox-composite="segmented" role=radiogroup aria-label data-iox-default-on [data-iox-scope]
33
+ * .iox-role-segmented-option role=radio aria-checked tabindex 0|-1 data-iox-item-key [data-iox-on]
34
+ *
35
+ * ← → move along the row (mirrored under RTL), ↑ ↓ the DOM order, each move wrapping and turning the option on; with no
36
+ * server state the option on is the root's default (`data-iox-default-on`).
28
37
  */
29
38
 
30
39
  export interface ExclusiveRuntimeOptions {
40
+ /** The kind it runs (else the root's `data-iox-composite`, else a Tabs) — the builder canvas knows it from the node
41
+ * before the attribute is on the element. */
42
+ kind?: ExclusiveCompositeKind;
31
43
  /** The scope id the ARIA ids are built from (else `data-iox-scope`, else the root's `iox-node-<id>`). A function is
32
44
  * read at every stamp (the builder canvas: a preview row's index is known once it is in the page). */
33
45
  rootId?: string | (() => string);
@@ -41,6 +53,9 @@ export interface ExclusiveRuntimeOptions {
41
53
  onChange?: (key: string) => void;
42
54
  /** Re-stamp when the markup under the root changes (the builder canvas re-renders parts). */
43
55
  observe?: boolean;
56
+ /** The key on when there is no server state (the builder canvas: a Segmented control's On by default), else the
57
+ * root's `data-iox-default-on`, else the first item. */
58
+ defaultKey?: () => string | null;
44
59
  }
45
60
 
46
61
  const KIND_ROOT = (kind: ExclusiveCompositeKind) => `[${COMPOSITE_ATTRIBUTE}="${kind}"]`;
@@ -104,14 +119,14 @@ export class ExclusiveRuntime {
104
119
  readonly kind: ExclusiveCompositeKind;
105
120
 
106
121
  constructor(readonly root: HTMLElement, private readonly options: ExclusiveRuntimeOptions = {}) {
107
- this.kind = kindOf(root);
122
+ this.kind = options.kind ?? kindOf(root);
108
123
  this.behaviourValue = options.behaviour ?? behaviourOf(root);
109
124
  root.setAttribute(COMPOSITE_ATTRIBUTE, this.kind);
110
125
  EXCLUSIVE_RUNTIMES.set(root, this);
111
- // The server rendered the first state (every trigger keyed, one on); with no server state the first is on.
126
+ // The server rendered the first state (every trigger keyed, one on); with no server state the default is on.
112
127
  const triggers = this.triggers();
113
128
  const serverOn = !options.behaviour ? triggers.find(t => t.hasAttribute(ON_ATTRIBUTE) && t.hasAttribute(ITEM_KEY_ATTRIBUTE)) : undefined;
114
- this.live = serverOn ? this.keyOf(serverOn) : (triggers[0] ? this.keyOf(triggers[0]) : null);
129
+ this.live = serverOn ? this.keyOf(serverOn) : this.defaultKey();
115
130
  this.awaitingItems = !triggers.length;
116
131
  // Capture: on the builder canvas every part's own listener stops the click (it selects that part), and the tab
117
132
  // must still turn on — the canvas is a live runtime.
@@ -205,6 +220,18 @@ export class ExclusiveRuntime {
205
220
  return this.triggers().map(t => this.keyOf(t));
206
221
  }
207
222
 
223
+ /** The key on with no server state: the asked default when it names an item, else the first item. */
224
+ private defaultKey(): string | null {
225
+ const keys = this.keys();
226
+ const asked = this.options.defaultKey?.() ?? this.root.getAttribute(DEFAULT_ON_ATTRIBUTE);
227
+ return asked && keys.includes(asked) ? asked : keys[0] ?? null;
228
+ }
229
+
230
+ /** True for a Segmented control: the root IS its radio group and holds its options. */
231
+ private get holdsTriggers(): boolean {
232
+ return exclusiveRootHoldsTriggers(this.kind);
233
+ }
234
+
208
235
  /** The element(s) that ARE the tablist: the ones a renderer already gave the role (the engine's component-path
209
236
  * Repeater puts it on its inner root, which holds the tabs), else the elements carrying the list's role class. */
210
237
  private lists(): HTMLElement[] {
@@ -234,12 +261,13 @@ export class ExclusiveRuntime {
234
261
  const keys = triggers.map(t => this.keyOf(t));
235
262
  if (this.awaitingItems && keys.length) {
236
263
  this.awaitingItems = false;
237
- this.live = keys[0];
264
+ this.live = this.defaultKey();
238
265
  first = true;
239
266
  }
240
- if (this.live === null || !keys.includes(this.live)) this.live = keys[0] ?? null;
267
+ if (this.live === null || !keys.includes(this.live)) this.live = this.defaultKey();
241
268
  const forced = this.options.forced?.() ?? null;
242
269
  const next = forced !== null && keys.includes(forced) ? forced : this.live;
270
+ if (this.holdsTriggers) return this.stampRadioGroup(triggers, next, first);
243
271
  const rootId = this.rootId();
244
272
  const vertical = this.behaviourValue.orientation === TabsOrientation.Vertical;
245
273
  for (const list of this.lists()) {
@@ -274,6 +302,28 @@ export class ExclusiveRuntime {
274
302
  setAttr(panel, ON_ATTRIBUTE, on ? '' : null);
275
303
  setAttr(panel, 'hidden', on ? null : '');
276
304
  }
305
+ this.announce(triggers, next, first, panelOf);
306
+ }
307
+
308
+ /** A Segmented control's markup (CC4b): the root a named radio group, each option a radio, one checked. */
309
+ private stampRadioGroup(options: HTMLElement[], next: string | null, first: boolean): void {
310
+ setAttr(this.root, 'role', 'radiogroup');
311
+ if (this.options.label) setAttr(this.root, 'aria-label', this.options.label() || null);
312
+ for (const option of options) {
313
+ const key = this.keyOf(option);
314
+ const on = key === next;
315
+ setAttr(option, ITEM_KEY_ATTRIBUTE, key);
316
+ setAttr(option, 'type', option.tagName === 'BUTTON' ? 'button' : null);
317
+ setAttr(option, 'role', 'radio');
318
+ setAttr(option, 'aria-checked', on ? 'true' : 'false');
319
+ setAttr(option, 'tabindex', on ? '0' : '-1');
320
+ setAttr(option, ON_ATTRIBUTE, on ? '' : null);
321
+ }
322
+ this.announce(options, next, first);
323
+ }
324
+
325
+ /** Move the shown key; dispatch iox-item-off on what was on and iox-item-on on what is on now. */
326
+ private announce(triggers: HTMLElement[], next: string | null, first: boolean, panelOf: (key: string) => HTMLElement | null = () => null): void {
277
327
  const was = first ? null : this.shown;
278
328
  this.shown = next;
279
329
  if (was === next && !first) return;
@@ -310,7 +360,7 @@ export class ExclusiveRuntime {
310
360
  const trigger = this.ownTrigger(e.target);
311
361
  if (!trigger) return;
312
362
  const rtl = isRtl(trigger);
313
- const action = tabsKeyAction(e.key, this.behaviourValue.orientation, rtl);
363
+ const action = this.holdsTriggers ? segmentedKeyAction(e.key, rtl) : tabsKeyAction(e.key, this.behaviourValue.orientation, rtl);
314
364
  if (!action) return;
315
365
  const triggers = this.triggers();
316
366
  const target = triggers[accordionFocusIndex(action, triggers.indexOf(trigger), triggers.length)];
@@ -0,0 +1,64 @@
1
+ import { DomAttributes } from './accordion';
2
+ import { CompositeKind, exclusiveKindOf } from './composite-kinds';
3
+ import { CompositeNode } from './composite-node';
4
+ import {
5
+ isSegmentedRoot, SEGMENTED_BEHAVIOUR, segmentedDefaultOn, segmentedLabel, segmentedOptionLabel, segmentedPartAttributes, segmentedRootAttributesOf,
6
+ segmentedScopeFor,
7
+ } from './segmented';
8
+ import { tabLabel, TabsBehaviour, tabsBehaviour, tabsLabel, tabsListOf, tabsPartAttributes, tabsRootAttributes, TabsScope, tabsScopeFor } from './tabs';
9
+
10
+ /**
11
+ * ONE render decision for every EXCLUSIVE composite — a Tabs (CC4a) or a Segmented control (CC4b) — so both engine paths
12
+ * (the string renderer and the component path) and the composer ask by kind once and never by role literal: the scope
13
+ * a root renders its parts with, the root's own attributes, and each part's.
14
+ */
15
+
16
+ export type ExclusiveScope = TabsScope;
17
+
18
+ const REPEATER = 'Repeater';
19
+
20
+ /** The scope an exclusive root renders its parts with: a Segmented control's options and default, or a Tabs' keys —
21
+ * From content, the rows its list's Repeater resolved (`rowsOf`). `rowKeys` = the repeated rows around the root. */
22
+ export function exclusiveScopeFor(root: CompositeNode, rootCssId: string, rowsOf: (list: CompositeNode) => ReadonlyArray<unknown> | null,
23
+ rowKeys: ReadonlyArray<string> = []): ExclusiveScope {
24
+ if (isSegmentedRoot(root)) return segmentedScopeFor(root, rootCssId, rowKeys);
25
+ const list = tabsListOf(root);
26
+ return tabsScopeFor(root, rootCssId, list?.type === REPEATER ? rowsOf(list) : null, rowKeys);
27
+ }
28
+
29
+ /** The attributes an exclusive ROOT writes on its own element, read off the node (its scope id when it sits in
30
+ * repeated rows). */
31
+ export function exclusiveRootAttributesOf(root: CompositeNode, scopeId?: string): DomAttributes {
32
+ return isSegmentedRoot(root) ? segmentedRootAttributesOf(root, scopeId) : tabsRootAttributes(tabsBehaviour(root), scopeId);
33
+ }
34
+
35
+ /** True when `node` is a part of the kind `scope` renders (a tab inside a Tabs' scope; an option inside a Segmented's). */
36
+ export function isPartOfExclusiveScope(node: { role?: string | null } | null | undefined, scope: ExclusiveScope): boolean {
37
+ const kind = exclusiveKindOf(node);
38
+ return !!kind && kind === (scope.kind ?? CompositeKind.Tabs);
39
+ }
40
+
41
+ /** The attributes a part of an exclusive composite writes on its own element (both engine paths, SSR included). */
42
+ export function exclusivePartAttributes(node: { role?: string | null }, scope: ExclusiveScope, key: string | null, rowScoped = false): DomAttributes {
43
+ return scope.kind === CompositeKind.Segmented ? segmentedPartAttributes(node, scope, key, rowScoped) : tabsPartAttributes(node, scope, key, rowScoped);
44
+ }
45
+
46
+ /** The behaviour an exclusive root runs with (a Segmented control: a click, a row — it has no Behaviour traits). */
47
+ export function exclusiveBehaviourOf(root: CompositeNode | null | undefined): TabsBehaviour {
48
+ return isSegmentedRoot(root) ? { ...SEGMENTED_BEHAVIOUR } : tabsBehaviour(root);
49
+ }
50
+
51
+ /** The accessible name an exclusive root gives its group (a Tabs' list, a Segmented control's radio group). */
52
+ export function exclusiveLabelOf(root: CompositeNode | null | undefined): string {
53
+ return isSegmentedRoot(root) ? segmentedLabel(root) : tabsLabel(root);
54
+ }
55
+
56
+ /** The item an exclusive root shows with no other say (a Segmented control's On by default), or null (a Tabs: its first). */
57
+ export function exclusiveDefaultOf(root: CompositeNode | null | undefined): string | null {
58
+ return isSegmentedRoot(root) ? segmentedDefaultOn(root) : null;
59
+ }
60
+
61
+ /** An exclusive item's words — a tab's, an option's (never its badge's) — for the pickers, the tree and the switchers. */
62
+ export function exclusiveItemLabel(part: CompositeNode): string {
63
+ return exclusiveKindOf(part) === CompositeKind.Segmented ? segmentedOptionLabel(part) : tabLabel(part);
64
+ }
@@ -3,8 +3,9 @@
3
3
  * plan §3a). FRAMEWORK-FREE: the builder, the SSR engine and iox-cms (generation-core's composer, the site
4
4
  * build) read the same recipes, roles and structure rules. Card v2 + Badge / Chip (T23 / T25) are the
5
5
  * first; the Accordion (CC2) adds the first composite RUNTIME (accordion-runtime.ts — DOM only, no
6
- * framework), which the Disclosure (CC2.1) runs too — a one-item accordion whose root is its item; Tabs (CC4a) run the
7
- * EXCLUSIVE runtime (exclusive-runtime.ts — one item always on); composite-runtime.ts is the one door to both.
6
+ * framework), which the Disclosure (CC2.1) runs too — a one-item accordion whose root is its item; Tabs (CC4a) and the
7
+ * Segmented control (CC4b) run the EXCLUSIVE runtime (exclusive-runtime.ts — one item always on; exclusive.ts the one
8
+ * render decision by kind); composite-runtime.ts is the one door to both families.
8
9
  * ⚠️ Consumers wire the usual trio — ng-package raw-ts assets, jest moduleNameMapper, tsconfig paths.
9
10
  */
10
11
  export * from './composite-node';
@@ -19,5 +20,7 @@ export * from './slider';
19
20
  export * from './slider-runtime';
20
21
  export * from './lightbox';
21
22
  export * from './tabs';
23
+ export * from './segmented';
24
+ export * from './exclusive';
22
25
  export * from './exclusive-runtime';
23
26
  export * from './composite-runtime';