@vectoriox/iox-ui 5.34.0 → 5.36.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.
- package/composite-core/index.d.ts +348 -2
- package/design-core/index.d.ts +417 -0
- package/fesm2022/vectoriox-iox-ui-composite-core.mjs +899 -2
- package/fesm2022/vectoriox-iox-ui-composite-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-design-core.mjs +489 -0
- package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-forms-core.mjs +2 -1
- package/fesm2022/vectoriox-iox-ui-forms-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs +138 -33
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui.mjs +122 -19
- package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
- package/generation-core/index.d.ts +53 -3
- package/index.d.ts +73 -10
- package/package.json +1 -1
- package/src/composite-core/accordion-runtime.ts +354 -0
- package/src/composite-core/accordion.ts +650 -0
- package/src/composite-core/composite-node.ts +5 -0
- package/src/composite-core/public-api.ts +4 -1
- package/src/composite-core/structure.ts +7 -0
- package/src/design-core/flavor-spec.generated.ts +489 -0
- package/src/forms-core/nesting.ts +2 -1
- package/src/generation-core/compose-site.ts +49 -1
- package/src/generation-core/node-baselines.generated.ts +31 -27
- package/src/generation-core/patterns.ts +10 -0
- package/src/generation-core/serialized-node.ts +4 -0
- package/src/generation-core/site-spec.generated.ts +46 -1
- package/src/generation-core/validate-site-plan.ts +5 -4
- package/styles/iox-design.css +30 -0
|
@@ -87,7 +87,7 @@ declare const SPEC_SITE_PLAN_SHAPE: {
|
|
|
87
87
|
readonly section: {
|
|
88
88
|
readonly pattern: "string — a non-global pattern id";
|
|
89
89
|
readonly "slots?": "{ [slot]: string } — the pattern's declared slots";
|
|
90
|
-
readonly "binding?": "string — the list source a pattern's $binding repeater iterates (exactly when it has one)";
|
|
90
|
+
readonly "binding?": "string — the list source a pattern's $binding repeater iterates (exactly when it has one); a $binding? pattern (faq.accordion) MAY take one — then its items come from that content";
|
|
91
91
|
readonly "without?": "string[] — the pattern's optional parts to leave out";
|
|
92
92
|
};
|
|
93
93
|
readonly globals: {
|
|
@@ -813,6 +813,45 @@ declare const SPEC_SITE_PATTERNS: {
|
|
|
813
813
|
}];
|
|
814
814
|
};
|
|
815
815
|
};
|
|
816
|
+
readonly "faq.accordion": {
|
|
817
|
+
readonly family: "faq";
|
|
818
|
+
readonly label: "FAQ — questions that open";
|
|
819
|
+
readonly node: {
|
|
820
|
+
readonly t: "Section";
|
|
821
|
+
readonly es: "section";
|
|
822
|
+
readonly c: readonly [{
|
|
823
|
+
readonly t: "Container";
|
|
824
|
+
readonly es: "box";
|
|
825
|
+
readonly lay: {
|
|
826
|
+
readonly display: "flex";
|
|
827
|
+
readonly flexDirection: "column";
|
|
828
|
+
readonly gap: "14px";
|
|
829
|
+
readonly maxWidth: "860px";
|
|
830
|
+
readonly width: "100%";
|
|
831
|
+
readonly margin: "0px auto 0px auto";
|
|
832
|
+
};
|
|
833
|
+
readonly c: readonly [{
|
|
834
|
+
readonly t: "Text";
|
|
835
|
+
readonly es: "eyebrow";
|
|
836
|
+
readonly slot: "eyebrow";
|
|
837
|
+
readonly ph: "copy";
|
|
838
|
+
readonly text: "FAQ";
|
|
839
|
+
}, {
|
|
840
|
+
readonly t: "Heading";
|
|
841
|
+
readonly es: "h2";
|
|
842
|
+
readonly level: "h2";
|
|
843
|
+
readonly slot: "title";
|
|
844
|
+
readonly ph: "copy";
|
|
845
|
+
readonly text: "Questions people ask";
|
|
846
|
+
}, {
|
|
847
|
+
readonly t: "Container";
|
|
848
|
+
readonly recipe: "accordion";
|
|
849
|
+
readonly src: "$binding?";
|
|
850
|
+
readonly ph: "copy";
|
|
851
|
+
}];
|
|
852
|
+
}];
|
|
853
|
+
};
|
|
854
|
+
};
|
|
816
855
|
};
|
|
817
856
|
declare const SPEC_SITE_TEMPLATES: {
|
|
818
857
|
readonly studio: {
|
|
@@ -1206,7 +1245,10 @@ interface PatternNode {
|
|
|
1206
1245
|
}
|
|
1207
1246
|
/** The composite-core recipes a pattern may stamp (`recipe`). */
|
|
1208
1247
|
declare enum PatternRecipe {
|
|
1209
|
-
Card = "card"
|
|
1248
|
+
Card = "card",
|
|
1249
|
+
/** CC2: the Accordion — Manual (three placeholder items), or From content when the section names a list
|
|
1250
|
+
* `binding` (`src: '$binding?'`): one template item bound to the type's question / answer fields. */
|
|
1251
|
+
Accordion = "accordion"
|
|
1210
1252
|
}
|
|
1211
1253
|
/** A recipe node's `binds` keys → the part each binds (Card v2). */
|
|
1212
1254
|
declare enum CardBindTarget {
|
|
@@ -1240,6 +1282,8 @@ declare enum PatternPlaceholder {
|
|
|
1240
1282
|
declare enum PatternDirective {
|
|
1241
1283
|
/** A Repeater's source: the section's `binding`. */
|
|
1242
1284
|
Binding = "$binding",
|
|
1285
|
+
/** The section MAY name a list `binding` (faq.accordion): with one, the recipe's items come from it. */
|
|
1286
|
+
OptionalBinding = "$binding?",
|
|
1243
1287
|
/** `each`: one plain node per page of the plan that is in the navigation (`inNav`), in plan order —
|
|
1244
1288
|
* words and routes written in, editable on the canvas (W1.1). */
|
|
1245
1289
|
NavPages = "$navPages",
|
|
@@ -1266,6 +1310,8 @@ declare function patternSlots(pattern: SitePattern): string[];
|
|
|
1266
1310
|
declare function patternOptionalParts(pattern: SitePattern): string[];
|
|
1267
1311
|
/** True when the pattern has a Repeater iterating the section's `binding`. */
|
|
1268
1312
|
declare function patternNeedsBinding(pattern: SitePattern): boolean;
|
|
1313
|
+
/** True when the pattern may take a section `binding` — it needs one, or names `$binding?` (CC2). */
|
|
1314
|
+
declare function patternTakesBinding(pattern: SitePattern): boolean;
|
|
1269
1315
|
/** The scopes whose ITEM a pattern's bound Images take their alt text from (an Image that names no `alt`
|
|
1270
1316
|
* of its own): `item` (the row of a `$binding` Repeater — the section's list), `route` or `next`. Each
|
|
1271
1317
|
* needs a content type with a `previewTextField` (W1.1, Q7). */
|
|
@@ -1331,6 +1377,10 @@ interface SerializedNode {
|
|
|
1331
1377
|
/** The part this node plays in a composite (composite-core: `card.title`; a navigation's `nav.drawer`,
|
|
1332
1378
|
* written by `planNavShape`). Additive; a node without one is a plain node. */
|
|
1333
1379
|
role?: string;
|
|
1380
|
+
/** An item's identity inside its composite (CC2: an Accordion item's `k1`…). Additive. */
|
|
1381
|
+
itemKey?: string;
|
|
1382
|
+
/** A targeted state's driving node (CC2: an icon's `trigger-on` → its item). */
|
|
1383
|
+
stateTriggers?: Partial<Record<string, string>>;
|
|
1334
1384
|
bindings: SerializedBinding[];
|
|
1335
1385
|
interactions: unknown[];
|
|
1336
1386
|
isGlobal?: boolean;
|
|
@@ -1657,5 +1707,5 @@ interface AddBarItemResult {
|
|
|
1657
1707
|
*/
|
|
1658
1708
|
declare function addBarItem(globals: ReadonlyArray<SerializedNode>, options?: Pick<NavShapeOptions, 'newId'>): AddBarItemResult;
|
|
1659
1709
|
|
|
1660
|
-
export { BAR_ITEMS_MAX, BAR_ITEMS_MIN, BASELINE_ROLES, BaselineRole, BindScope, CardBindTarget, ComposeError, ComposeIssueCode, DANGLING_LINK_CODES, DEFAULT_PHONE_MENU_BREAKPOINT, FACT_KEYS, GlobalSlot, NAV_PART_LABELS, NAV_SHAPE_COPY, NEUTRAL_STYLE_PROPS, NODE_BASELINES, NavRole, NavShape, NavShapeError, PatternDirective, PatternGlobalPosition, PatternPlaceholder, PatternRecipe, PhoneMenuBreakpoint, PlaceholderKind, PlanDataSourceMode, PlanDataSourceType, RelativeOp, SAMPLE_IMAGE_REF, SITE_FACT_LABELS, SITE_PATTERNS, SITE_PLAN_VERSION, SITE_TEMPLATES, SPEC_SITE_FACTS, SPEC_SITE_PATTERNS, SPEC_SITE_PLAN_SHAPE, SPEC_SITE_TEMPLATES, STUDIO_TEMPLATE, SitePlanError, SitePlanIssueCode, SitePlanSourceKind, SiteTemplateError, SiteTemplateId, addBarItem, altSourceScopes, assertStyleSlots, assertValidSitePlan, barIconForRoute, composeSite, createSerializedNode, findNavRoot, navPages, navShapeBreakpoint, navShapeOf, nextSourceOf, nodeBindPaths, parseSampleImageRef, patternById, patternNeedsBinding, patternNodes, patternOptionalParts, patternScopes, patternSlots, planFromTemplate, planNavShape, releasedLayoutDefaults, requiredShapeKeys, routeParams, sampleImageRef, setStyleProps, shapeKeys, validateSitePlan, walkSerialized };
|
|
1710
|
+
export { BAR_ITEMS_MAX, BAR_ITEMS_MIN, BASELINE_ROLES, BaselineRole, BindScope, CardBindTarget, ComposeError, ComposeIssueCode, DANGLING_LINK_CODES, DEFAULT_PHONE_MENU_BREAKPOINT, FACT_KEYS, GlobalSlot, NAV_PART_LABELS, NAV_SHAPE_COPY, NEUTRAL_STYLE_PROPS, NODE_BASELINES, NavRole, NavShape, NavShapeError, PatternDirective, PatternGlobalPosition, PatternPlaceholder, PatternRecipe, PhoneMenuBreakpoint, PlaceholderKind, PlanDataSourceMode, PlanDataSourceType, RelativeOp, SAMPLE_IMAGE_REF, SITE_FACT_LABELS, SITE_PATTERNS, SITE_PLAN_VERSION, SITE_TEMPLATES, SPEC_SITE_FACTS, SPEC_SITE_PATTERNS, SPEC_SITE_PLAN_SHAPE, SPEC_SITE_TEMPLATES, STUDIO_TEMPLATE, SitePlanError, SitePlanIssueCode, SitePlanSourceKind, SiteTemplateError, SiteTemplateId, addBarItem, altSourceScopes, assertStyleSlots, assertValidSitePlan, barIconForRoute, composeSite, createSerializedNode, findNavRoot, navPages, navShapeBreakpoint, navShapeOf, nextSourceOf, nodeBindPaths, parseSampleImageRef, patternById, patternNeedsBinding, patternNodes, patternOptionalParts, patternScopes, patternSlots, patternTakesBinding, planFromTemplate, planNavShape, releasedLayoutDefaults, requiredShapeKeys, routeParams, sampleImageRef, setStyleProps, shapeKeys, validateSitePlan, walkSerialized };
|
|
1661
1711
|
export type { AddBarItemResult, CmsDataSourcePlan, ComposeContext, ComposeIssue, ComposedDataSource, ComposedItem, ComposedPage, ComposedPlaceholder, ComposedRoute, ComposedSite, ContentFieldPlan, ContentSeedPlan, ContentTypePlan, CreateNodeOptions, DataSourcePlan, FactKey, NavPage, NavShapeOptions, NavShapeResult, NodeBaseline, PagePlan, PageSectionPlan, PatternNode, RelativeDataSourcePlan, SerializedBinding, SerializedNode, SerializedResponsive, SiteBusiness, SitePattern, SitePlan, SitePlanIssue, SitePlanSource, SitePlanValidation, SiteTemplate, TemplatePlanInput };
|
package/index.d.ts
CHANGED
|
@@ -379,7 +379,7 @@ declare class BuilderHeadingComponent implements OnInit {
|
|
|
379
379
|
private isBound;
|
|
380
380
|
private syncTraitToPanel;
|
|
381
381
|
static ɵfac: i0.ɵɵFactoryDeclaration<BuilderHeadingComponent, [{ optional: true; }, null, { optional: true; }]>;
|
|
382
|
-
static ɵcmp: i0.ɵɵComponentDeclaration<BuilderHeadingComponent, "h1[iox-heading],h2[iox-heading],h3[iox-heading],h4[iox-heading],h5[iox-heading],h6[iox-heading]", never, { "content": { "alias": "content"; "required": false; }; "level": { "alias": "level"; "required": false; }; "nodeId": { "alias": "nodeId"; "required": false; }; "node": { "alias": "node"; "required": false; }; "linkType": { "alias": "linkType"; "required": false; }; "url": { "alias": "url"; "required": false; }; "pageRoute": { "alias": "pageRoute"; "required": false; }; "target": { "alias": "target"; "required": false; }; }, {}, never, never, false, never>;
|
|
382
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<BuilderHeadingComponent, "h1[iox-heading],h2[iox-heading],h3[iox-heading],h4[iox-heading],h5[iox-heading],h6[iox-heading],span[iox-heading]", never, { "content": { "alias": "content"; "required": false; }; "level": { "alias": "level"; "required": false; }; "nodeId": { "alias": "nodeId"; "required": false; }; "node": { "alias": "node"; "required": false; }; "linkType": { "alias": "linkType"; "required": false; }; "url": { "alias": "url"; "required": false; }; "pageRoute": { "alias": "pageRoute"; "required": false; }; "target": { "alias": "target"; "required": false; }; }, {}, never, never, false, never>;
|
|
383
383
|
}
|
|
384
384
|
|
|
385
385
|
interface EditableNode {
|
|
@@ -447,7 +447,11 @@ declare class ContainerComponent {
|
|
|
447
447
|
static ɵcmp: i0.ɵɵComponentDeclaration<ContainerComponent, "div[iox-container],section[iox-container],article[iox-container],header[iox-container],footer[iox-container],main[iox-container],nav[iox-container],ul[iox-container],ol[iox-container],li[iox-container]", never, { "nodeId": { "alias": "nodeId"; "required": false; }; }, {}, never, never, false, never>;
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
-
/**
|
|
450
|
+
/**
|
|
451
|
+
* Directive-host linked container: the `<a>` IS the host — no wrapper, no inner/outer split. An Accordion's
|
|
452
|
+
* HEADER (CC2, role `accordion.trigger`) is created on a `<button>` instead — a button that opens its item,
|
|
453
|
+
* never a link; the engine puts it inside the `<hN>` its question's level names.
|
|
454
|
+
*/
|
|
451
455
|
declare class LinkedContainerComponent implements OnInit, OnDestroy {
|
|
452
456
|
private elRef;
|
|
453
457
|
private isPreview;
|
|
@@ -461,9 +465,11 @@ declare class LinkedContainerComponent implements OnInit, OnDestroy {
|
|
|
461
465
|
constructor(elRef: ElementRef<HTMLElement>, isPreview: boolean);
|
|
462
466
|
ngOnInit(): void;
|
|
463
467
|
ngOnDestroy(): void;
|
|
468
|
+
/** True when created on a <button> (an Accordion header) — it never navigates. */
|
|
469
|
+
get isButton(): boolean;
|
|
464
470
|
get resolvedHref(): string;
|
|
465
471
|
static ɵfac: i0.ɵɵFactoryDeclaration<LinkedContainerComponent, [null, { optional: true; }]>;
|
|
466
|
-
static ɵcmp: i0.ɵɵComponentDeclaration<LinkedContainerComponent, "a[iox-linked-container]", never, { "nodeId": { "alias": "nodeId"; "required": false; }; "linkType": { "alias": "linkType"; "required": false; }; "url": { "alias": "url"; "required": false; }; "pageRoute": { "alias": "pageRoute"; "required": false; }; "target": { "alias": "target"; "required": false; }; }, {}, never, never, false, never>;
|
|
472
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<LinkedContainerComponent, "a[iox-linked-container],button[iox-linked-container]", never, { "nodeId": { "alias": "nodeId"; "required": false; }; "linkType": { "alias": "linkType"; "required": false; }; "url": { "alias": "url"; "required": false; }; "pageRoute": { "alias": "pageRoute"; "required": false; }; "target": { "alias": "target"; "required": false; }; }, {}, never, never, false, never>;
|
|
467
473
|
}
|
|
468
474
|
|
|
469
475
|
/**
|
|
@@ -1515,12 +1521,22 @@ declare function statePseudoClass(state: string): string;
|
|
|
1515
1521
|
* so the look and the announcement are one fact.
|
|
1516
1522
|
*/
|
|
1517
1523
|
declare enum ComponentState {
|
|
1518
|
-
Current = "current"
|
|
1519
|
-
|
|
1520
|
-
|
|
1524
|
+
Current = "current",
|
|
1525
|
+
/** A composite part that IS on — an open accordion item, its header, its panel (board 9 "Open"). */
|
|
1526
|
+
On = "on",
|
|
1527
|
+
/** A part INSIDE an item, styled by its item being open (board 9 "Item open") — a TARGETED state: its
|
|
1528
|
+
* trigger is the item (`stateTriggers['trigger-on']`), compiled through `targetedStateSelector`. */
|
|
1529
|
+
ItemOpen = "trigger-on"
|
|
1530
|
+
}
|
|
1531
|
+
/** The attribute selector each (non-targeted) component state compiles to. */
|
|
1521
1532
|
declare const COMPONENT_STATE_SELECTOR: Readonly<Record<string, string>>;
|
|
1522
|
-
/** The
|
|
1523
|
-
declare
|
|
1533
|
+
/** The composite roles that are themselves "on" when their item is open (they carry `data-iox-on`). */
|
|
1534
|
+
declare const ON_STATE_ROLES: ReadonlyArray<string>;
|
|
1535
|
+
/**
|
|
1536
|
+
* The component states a node offers in the Style panel's "Component state" group: Current page (a link),
|
|
1537
|
+
* Open (an accordion's item, header or panel), Item open (any other part inside an item — `insideItem`).
|
|
1538
|
+
*/
|
|
1539
|
+
declare function componentStatesFor(type: string | null | undefined, role?: string | null, insideItem?: boolean): ComponentState[];
|
|
1524
1540
|
/**
|
|
1525
1541
|
* What follows `.iox-node-<id>` in a stored state's rule: `:hover`, `:focus-visible`, or a component
|
|
1526
1542
|
* state's attribute selector (`[aria-current="page"]`). Shared by the builder canvas and both engine paths.
|
|
@@ -1607,6 +1623,21 @@ declare const LEGACY_DIRECTION_KEY = "direction";
|
|
|
1607
1623
|
* node renders. Pure; returns the map unchanged (same reference) when there is nothing to move.
|
|
1608
1624
|
*/
|
|
1609
1625
|
declare function migrateLegacyDirection(styleProps: Record<string, any>): Record<string, any>;
|
|
1626
|
+
/** The Align trait's values: LOGICAL, so a text starts at the inline start of whatever direction it
|
|
1627
|
+
* reads in — the left in LTR, the right in RTL (vectoriox/iox-ui#16). */
|
|
1628
|
+
declare enum TextAlign {
|
|
1629
|
+
Start = "start",
|
|
1630
|
+
Center = "center",
|
|
1631
|
+
End = "end",
|
|
1632
|
+
Justify = "justify"
|
|
1633
|
+
}
|
|
1634
|
+
declare const TEXT_ALIGN_KEY = "textAlign";
|
|
1635
|
+
/**
|
|
1636
|
+
* A saved node's own style map with the serialised default `textAlign: left` read as the logical
|
|
1637
|
+
* `start` it compiles to — so the builder's panel highlights Start, and the next save stores it.
|
|
1638
|
+
* Pure; returns the map unchanged (same reference) when there is nothing to move.
|
|
1639
|
+
*/
|
|
1640
|
+
declare function migrateLegacyTextAlign(styleProps: Record<string, any>): Record<string, any>;
|
|
1610
1641
|
declare function isGradientPaint(value: unknown): boolean;
|
|
1611
1642
|
/** The whole-value backdrop trait (DS2). A plain key — it is copied, not stripped — but it feeds
|
|
1612
1643
|
* the backdrop group's output, so a breakpoint layer that sets it must count as touching it. */
|
|
@@ -1682,6 +1713,20 @@ declare function nearestCommonAncestorId(root: readonly StyleTreeNode[], styledC
|
|
|
1682
1713
|
* `scopeCssId` MUST be a value returned by `nearestCommonAncestorId` for the same pair.
|
|
1683
1714
|
*/
|
|
1684
1715
|
declare function buildTargetedStateSelector(scopeCssId: string, triggerCssId: string, styledCssId: string, base: string): string;
|
|
1716
|
+
/**
|
|
1717
|
+
* The same three topologies for any trigger SUFFIX — a pseudo-class (`:hover`) or an attribute the renderers
|
|
1718
|
+
* write (`[data-iox-on]`, CC2's "Item open").
|
|
1719
|
+
*/
|
|
1720
|
+
declare function buildTargetedSuffixSelector(scopeCssId: string, triggerCssId: string, styledCssId: string, suffix: string): string;
|
|
1721
|
+
/**
|
|
1722
|
+
* The targeted states and what follows the TRIGGER's class in each: `trigger-hover` — the chosen element is
|
|
1723
|
+
* hovered; `trigger-on` — the chosen item is open (CC2: an Icon turns when ITS item opens; the trigger is the
|
|
1724
|
+
* item, an ancestor, so the rule is `.item[data-iox-on] .icon`). Shared by the builder canvas and both
|
|
1725
|
+
* engine paths.
|
|
1726
|
+
*/
|
|
1727
|
+
declare const TARGETED_STATE_SUFFIX: Readonly<Record<string, string>>;
|
|
1728
|
+
/** The selector body of a targeted state's rule, or null for a state that is not targeted. */
|
|
1729
|
+
declare function targetedStateSelector(state: string, scopeCssId: string, triggerCssId: string, styledCssId: string): string | null;
|
|
1685
1730
|
|
|
1686
1731
|
/**
|
|
1687
1732
|
* parts — the failure design (forms-and-inputs-plan.md Part 2, board 7). A compound component
|
|
@@ -2054,9 +2099,26 @@ interface DataSourceQueryFilter {
|
|
|
2054
2099
|
* fallback too. An empty default with no query-param value means the filter is DROPPED. */
|
|
2055
2100
|
defaultValue?: string;
|
|
2056
2101
|
}
|
|
2102
|
+
/** The order a query source returns its rows in (CC2, board 4 "Sort" — the data source popover). */
|
|
2103
|
+
declare enum DataSourceSortDir {
|
|
2104
|
+
Asc = "asc",
|
|
2105
|
+
Desc = "desc"
|
|
2106
|
+
}
|
|
2107
|
+
interface DataSourceSort {
|
|
2108
|
+
/** A field of the content type (a dot path into a nested object is allowed). */
|
|
2109
|
+
field?: string;
|
|
2110
|
+
dir?: DataSourceSortDir | `${DataSourceSortDir}`;
|
|
2111
|
+
}
|
|
2112
|
+
/** The query parameter carrying the order: `sort=order` ascending, `sort=-order` descending. A CONTROL like
|
|
2113
|
+
* `limit`, not a field filter — the public controllers split it out (iox-cms `query-controls.ts`). */
|
|
2114
|
+
declare const SORT_PARAM = "sort";
|
|
2115
|
+
/** `sort`'s wire value, or null when the source sets no (valid) order. */
|
|
2116
|
+
declare function sortParamValue(sort: DataSourceSort | null | undefined): string | null;
|
|
2057
2117
|
interface DataSourceRequestSpec {
|
|
2058
2118
|
contentType?: string;
|
|
2059
2119
|
limit?: number;
|
|
2120
|
+
/** A query source's order (CC2). Absent = the backend's natural order, as before. */
|
|
2121
|
+
sort?: DataSourceSort;
|
|
2060
2122
|
filterByRouteParam?: DataSourceRouteFilter;
|
|
2061
2123
|
filterByQueryParams?: DataSourceQueryFilter[];
|
|
2062
2124
|
}
|
|
@@ -2091,6 +2153,7 @@ type DataSourcePlan = {
|
|
|
2091
2153
|
resource: string;
|
|
2092
2154
|
filter: Record<string, any>;
|
|
2093
2155
|
limit?: number;
|
|
2156
|
+
sort?: string;
|
|
2094
2157
|
mode: DataSourceMode;
|
|
2095
2158
|
} | {
|
|
2096
2159
|
kind: 'external';
|
|
@@ -3152,5 +3215,5 @@ declare class GalleryLoop {
|
|
|
3152
3215
|
private emit;
|
|
3153
3216
|
}
|
|
3154
3217
|
|
|
3155
|
-
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, 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, LEGACY_DIRECTION_KEY, LEGACY_HOVER_TRANSITION, LinkedContainerComponent, MANAGED_A11Y_ATTRIBUTES, MAX_LINES_KEY, PAGE_STYLE_TOKENS, PAGE_TOKEN_FALLBACKS, PAGE_TRANSITION_CATEGORIES, PAGE_TRANSITION_PRESETS, PART_CLASS_NAMES, PIPE_DESCRIPTORS, PIPE_REGISTRY, PageScrollProvider, PageStyleField, PageStyleSource, REDUCED_MOTION_QUERY, RESERVED_ALIAS_PREFIX, STATE_PSEUDO_CLASS, SUPPRESSED_STYLE_PROPS_BY_TYPE, SWIPER_A11Y_OPTIONS, SWIPER_RESPONSIVE_TRAITS, SectionComponent, SwiperModuleName, TEXT_DIRECTION_KEY, TRANSITION_FEELS, TextBlockComponent, VALIDATION_STATES, VALIDATION_STATE_CLASS, VIRTUAL_TRAIT_GROUPS, VIRTUAL_TRAIT_KEYS, ViewPositionDirective, ViewPositionModule, VisibilityActionType, activeActions, applyBindingPipes, applyFeel, breakpointDef, breakpointForWidth, breakpointQueryFor, buildDataSourceFilter, buildInteractionKeyframes, buildPartSelector, buildRelatedFilter, buildSwiperBreakpoints, buildTargetedStateSelector, 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, 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, motionAllowed, nearestCommonAncestorId, needsAccessibleName, nodeA11yAttributes, nodeCssId, nodeTraitValue, nodeUsesContext, normalizeAppPath, normalizeCollectionResult, normalizeGalleryImage, normalizeIconClass, parentBreakpoint, parseUrlList, partClassName, planDataSource, planResponsiveRules, 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, stateNamesIn, statePseudoClass, stateSelectorSuffix, stripSuppressedProps, swiperAutoplayAllowed, swiperModuleNames, syncNodeA11yAttributes, variableAliases, variableItemsFrom, withContextSlots, withDefaultHoverTransition, wrapInBreakpointQuery, wrapInMediaQuery };
|
|
3156
|
-
export type { A11yNodeSource, BoundValueSpec, BreakpointDef, BuildRelatedFilterOptions, ContextActionEffect, ContextActionLike, ContextItemRef, ContextSlotDefault, ContextValueSpec, ContextWriteScope, CurrentLinkOptions, DataSourceDomain, DataSourceEndpoints, DataSourceMode, DataSourcePlan, DataSourceQueryFilter, DataSourceRequest, DataSourceRequestSpec, DataSourceRouteFilter, 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, StateStyleMap, StyleMap, StyleTreeNode, SwiperBreakpointMap, SwiperModuleFlags, ToggleableAction, TransitionFeel, TransitionPhase, TransitionTimelineInput, ValueSource, VirtualTraitGroup };
|
|
3218
|
+
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, LEGACY_DIRECTION_KEY, LEGACY_HOVER_TRANSITION, 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, PIPE_DESCRIPTORS, PIPE_REGISTRY, PageScrollProvider, PageStyleField, PageStyleSource, REDUCED_MOTION_QUERY, RESERVED_ALIAS_PREFIX, SORT_PARAM, STATE_PSEUDO_CLASS, SUPPRESSED_STYLE_PROPS_BY_TYPE, SWIPER_A11Y_OPTIONS, SWIPER_RESPONSIVE_TRAITS, 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, 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, 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, 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, sortParamValue, stateNamesIn, statePseudoClass, stateSelectorSuffix, stripSuppressedProps, swiperAutoplayAllowed, swiperModuleNames, syncNodeA11yAttributes, targetedStateSelector, variableAliases, variableItemsFrom, withContextSlots, withDefaultHoverTransition, wrapInBreakpointQuery, wrapInMediaQuery };
|
|
3219
|
+
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, StateStyleMap, StyleMap, StyleTreeNode, SwiperBreakpointMap, SwiperModuleFlags, ToggleableAction, TransitionFeel, TransitionPhase, TransitionTimelineInput, ValueSource, VirtualTraitGroup };
|
package/package.json
CHANGED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AccordionBehaviour, AccordionClass, AccordionDefaultOn, accordionDomIds, accordionFocusIndex, accordionKeyAction, AccordionRole,
|
|
3
|
+
ACCORDION_DEFAULT_ONS, COMPOSITE_ATTRIBUTE, DEFAULT_ON_ATTRIBUTE, initialAccordionOpen, ITEM_KEY_ATTRIBUTE, MULTIPLE_ATTRIBUTE,
|
|
4
|
+
nextAccordionState, ON_ATTRIBUTE, openAccordionItem, UNTIL_FOUND,
|
|
5
|
+
} from './accordion';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The Accordion's RUNTIME (CC2) — ONE copy, DOM only, for the builder canvas and the live site (both engine
|
|
9
|
+
* paths render the same markup). It reads the markup the renderers write (iox-design.css + accordion.ts's
|
|
10
|
+
* attribute planners), so the server's first paint and every later state agree:
|
|
11
|
+
*
|
|
12
|
+
* [root] data-iox-composite="accordion" data-iox-default-on [data-iox-multiple] (the node's own element —
|
|
13
|
+
* its role class may sit on a wrapper host, as the engine's component-path Repeater draws it)
|
|
14
|
+
* .iox-role-accordion-item data-iox-item-key [data-iox-on]
|
|
15
|
+
* hN.iox-accordion-heading > button.iox-accordion-button id aria-expanded aria-controls
|
|
16
|
+
* .iox-accordion-track [data-iox-on] > .iox-accordion-region role=region [hidden=until-found]
|
|
17
|
+
* .iox-role-accordion-panel
|
|
18
|
+
*
|
|
19
|
+
* It owns: a click on a header toggles its item; ↓ ↑ Home End move between the headers of THIS accordion
|
|
20
|
+
* (a nested one keeps its own); find-in-page (`beforematch`) opens the item that holds the match; a closed
|
|
21
|
+
* panel gets `hidden="until-found"` after its track has collapsed (at once under reduced motion). Every
|
|
22
|
+
* change dispatches `iox-item-on` / `iox-item-off` on the item, the `itemOn` / `itemOff` interaction
|
|
23
|
+
* triggers. The open set is LIVE ∪ FORCED: the builder forces items open (a selection inside a closed panel,
|
|
24
|
+
* "All open"); the live site forces nothing.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export const ITEM_ON_EVENT = 'iox-item-on';
|
|
28
|
+
export const ITEM_OFF_EVENT = 'iox-item-off';
|
|
29
|
+
|
|
30
|
+
const ROOT_SELECTOR = `[${COMPOSITE_ATTRIBUTE}="${AccordionRole.Accordion}"]`;
|
|
31
|
+
const ITEM_SELECTOR = '.iox-role-accordion-item';
|
|
32
|
+
const PANEL_SELECTOR = '.iox-role-accordion-panel';
|
|
33
|
+
const BUTTON_SELECTOR = `.${AccordionClass.Button}`;
|
|
34
|
+
const TRACK_SELECTOR = `.${AccordionClass.Track}`;
|
|
35
|
+
const REGION_SELECTOR = `.${AccordionClass.Region}`;
|
|
36
|
+
/** How long a closing track may take before its region is hidden (dur-base is 200–320ms in every pack). */
|
|
37
|
+
const MAX_CLOSE_MS = 600;
|
|
38
|
+
const REDUCED_MOTION = '(prefers-reduced-motion: reduce)';
|
|
39
|
+
|
|
40
|
+
export interface AccordionRuntimeOptions {
|
|
41
|
+
/** The root's node id — the ARIA ids' prefix (else read from the root's `iox-node-<id>` class). */
|
|
42
|
+
rootId?: string;
|
|
43
|
+
/** The behaviour (else read from the root's data attributes). */
|
|
44
|
+
behaviour?: AccordionBehaviour;
|
|
45
|
+
/** Keys forced open on top of the live state (the builder's design state). */
|
|
46
|
+
forced?: () => ReadonlySet<string>;
|
|
47
|
+
/** Re-stamp when the markup under the root changes (the builder canvas re-renders parts). */
|
|
48
|
+
observe?: boolean;
|
|
49
|
+
/** True to stop motion (default: the visitor's reduced-motion setting). */
|
|
50
|
+
reducedMotion?: () => boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The accordion an element belongs to (the element itself when it is a root). */
|
|
54
|
+
export function closestAccordion(el: Element | null | undefined): HTMLElement | null {
|
|
55
|
+
return (el?.closest?.(ROOT_SELECTOR) as HTMLElement | null) ?? null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The item an element sits in (itself when it is one). */
|
|
59
|
+
export function closestAccordionItem(el: Element | null | undefined): HTMLElement | null {
|
|
60
|
+
return (el?.closest?.(ITEM_SELECTOR) as HTMLElement | null) ?? null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Subscribe to an item turning on / off — what the `itemOn` / `itemOff` interaction triggers listen to. The
|
|
64
|
+
* item is the element's own item (the element itself when it is one). Returns the unsubscribe. */
|
|
65
|
+
export function onAccordionItemState(el: Element, on: boolean, handler: () => void): () => void {
|
|
66
|
+
const item = closestAccordionItem(el);
|
|
67
|
+
if (!item) return () => undefined;
|
|
68
|
+
const type = on ? ITEM_ON_EVENT : ITEM_OFF_EVENT;
|
|
69
|
+
item.addEventListener(type, handler);
|
|
70
|
+
return () => item.removeEventListener(type, handler);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** True when the element's item is open now. */
|
|
74
|
+
export function isAccordionItemOn(el: Element): boolean {
|
|
75
|
+
return !!closestAccordionItem(el)?.hasAttribute(ON_ATTRIBUTE);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const nodeIdOf = (el: Element): string => {
|
|
79
|
+
for (const c of Array.from(el.classList)) if (c.startsWith('iox-node-')) return c.slice('iox-node-'.length);
|
|
80
|
+
return '';
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
function behaviourOf(root: Element): AccordionBehaviour {
|
|
84
|
+
const raw = root.getAttribute(DEFAULT_ON_ATTRIBUTE);
|
|
85
|
+
return {
|
|
86
|
+
multiple: root.hasAttribute(MULTIPLE_ATTRIBUTE),
|
|
87
|
+
defaultOn: ACCORDION_DEFAULT_ONS.includes(raw as AccordionDefaultOn) ? raw as AccordionDefaultOn : AccordionDefaultOn.First,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function setAttr(el: Element | null | undefined, name: string, value: string | null): void {
|
|
92
|
+
if (!el) return;
|
|
93
|
+
if (value === null) { if (el.hasAttribute(name)) el.removeAttribute(name); }
|
|
94
|
+
else if (el.getAttribute(name) !== value) el.setAttribute(name, value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export class AccordionRuntime {
|
|
98
|
+
private live: Set<string>;
|
|
99
|
+
private shown = new Set<string>();
|
|
100
|
+
private readonly closing = new Map<string, ReturnType<typeof setTimeout>>();
|
|
101
|
+
private observer: MutationObserver | null = null;
|
|
102
|
+
private stampQueued = false;
|
|
103
|
+
private behaviourValue: AccordionBehaviour;
|
|
104
|
+
/** The builder attaches a root before its items render: the saved default is applied once they do. */
|
|
105
|
+
private awaitingItems = false;
|
|
106
|
+
|
|
107
|
+
constructor(readonly root: HTMLElement, private readonly options: AccordionRuntimeOptions = {}) {
|
|
108
|
+
this.behaviourValue = options.behaviour ?? behaviourOf(root);
|
|
109
|
+
root.setAttribute(COMPOSITE_ATTRIBUTE, AccordionRole.Accordion);
|
|
110
|
+
// The server rendered the first state; with no server state the root's default decides.
|
|
111
|
+
// (The builder passes the behaviour and starts from the saved default; on the live site the server
|
|
112
|
+
// stamped every item's key and state, and that is the truth.)
|
|
113
|
+
const items = this.items();
|
|
114
|
+
const serverStamped = !options.behaviour && items.some(i => i.hasAttribute(ITEM_KEY_ATTRIBUTE));
|
|
115
|
+
this.live = serverStamped
|
|
116
|
+
? new Set(items.filter(i => i.hasAttribute(ON_ATTRIBUTE)).map(i => this.keyOf(i, items)))
|
|
117
|
+
: initialAccordionOpen(items.map(i => this.keyOf(i, items)), this.behaviourValue.defaultOn);
|
|
118
|
+
this.awaitingItems = !serverStamped && !items.length;
|
|
119
|
+
this.shown = new Set(this.live);
|
|
120
|
+
// Capture: on the builder canvas every part's own listener stops the click (it selects that part), and
|
|
121
|
+
// the header must still toggle — the canvas is a live runtime.
|
|
122
|
+
root.addEventListener('click', this.onClick, true);
|
|
123
|
+
root.addEventListener('keydown', this.onKeydown);
|
|
124
|
+
root.addEventListener('beforematch', this.onBeforeMatch, true);
|
|
125
|
+
if (options.observe && typeof MutationObserver !== 'undefined') {
|
|
126
|
+
this.observer = new MutationObserver(() => this.queueStamp());
|
|
127
|
+
this.observer.observe(root, { childList: true, subtree: true });
|
|
128
|
+
}
|
|
129
|
+
this.stamp(true);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
get behaviour(): AccordionBehaviour {
|
|
133
|
+
return this.behaviourValue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The behaviour changed in the panel: re-read, and start from the new default. */
|
|
137
|
+
setBehaviour(behaviour: AccordionBehaviour): void {
|
|
138
|
+
const changedDefault = behaviour.defaultOn !== this.behaviourValue.defaultOn;
|
|
139
|
+
this.behaviourValue = behaviour;
|
|
140
|
+
if (behaviour.multiple) this.root.setAttribute(MULTIPLE_ATTRIBUTE, '');
|
|
141
|
+
else this.root.removeAttribute(MULTIPLE_ATTRIBUTE);
|
|
142
|
+
this.root.setAttribute(DEFAULT_ON_ATTRIBUTE, behaviour.defaultOn);
|
|
143
|
+
if (changedDefault) this.resetLive();
|
|
144
|
+
else this.stamp();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** THIS accordion's items, in document order (a nested accordion's items are its own). */
|
|
148
|
+
items(): HTMLElement[] {
|
|
149
|
+
return Array.from(this.root.querySelectorAll<HTMLElement>(ITEM_SELECTOR)).filter(i => closestAccordion(i.parentElement) === this.root);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The open keys the live state holds (before anything forced). */
|
|
153
|
+
get liveKeys(): ReadonlySet<string> {
|
|
154
|
+
return this.live;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The keys showing open right now. */
|
|
158
|
+
get openKeys(): ReadonlySet<string> {
|
|
159
|
+
return this.shown;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
keyOf(item: Element, items: HTMLElement[] = this.items()): string {
|
|
163
|
+
return item.getAttribute(ITEM_KEY_ATTRIBUTE) || `item-${items.indexOf(item as HTMLElement)}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
toggle(key: string): void {
|
|
167
|
+
this.live = nextAccordionState(this.live, key, this.behaviourValue);
|
|
168
|
+
this.stamp();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
open(key: string): void {
|
|
172
|
+
this.live = openAccordionItem(this.live, key, this.behaviourValue);
|
|
173
|
+
this.stamp();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Back to the saved default ("Showing on canvas ▸ As saved"). */
|
|
177
|
+
resetLive(): void {
|
|
178
|
+
const items = this.items();
|
|
179
|
+
this.live = initialAccordionOpen(items.map(i => this.keyOf(i, items)), this.behaviourValue.defaultOn);
|
|
180
|
+
this.stamp();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** The forced set changed (builder design state): show it. */
|
|
184
|
+
refresh(): void {
|
|
185
|
+
this.stamp();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
destroy(): void {
|
|
189
|
+
this.root.removeEventListener('click', this.onClick, true);
|
|
190
|
+
this.root.removeEventListener('keydown', this.onKeydown);
|
|
191
|
+
this.root.removeEventListener('beforematch', this.onBeforeMatch, true);
|
|
192
|
+
this.observer?.disconnect();
|
|
193
|
+
this.observer = null;
|
|
194
|
+
for (const t of this.closing.values()) clearTimeout(t);
|
|
195
|
+
this.closing.clear();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private queueStamp(): void {
|
|
199
|
+
if (this.stampQueued) return;
|
|
200
|
+
this.stampQueued = true;
|
|
201
|
+
const run = () => { this.stampQueued = false; this.stamp(); };
|
|
202
|
+
if (typeof queueMicrotask === 'function') queueMicrotask(run); else Promise.resolve().then(run);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private reduced(): boolean {
|
|
206
|
+
if (this.options.reducedMotion) return this.options.reducedMotion();
|
|
207
|
+
try { return typeof matchMedia === 'function' && matchMedia(REDUCED_MOTION).matches; } catch { return false; }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Write every item's state into its markup; announce what changed. */
|
|
211
|
+
private stamp(first = false): void {
|
|
212
|
+
// Our own writes must not wake the observer (they are attributes; it watches the child lists only).
|
|
213
|
+
const items = this.items();
|
|
214
|
+
if (this.awaitingItems && items.length) {
|
|
215
|
+
this.awaitingItems = false;
|
|
216
|
+
this.live = initialAccordionOpen(items.map(i => this.keyOf(i, items)), this.behaviourValue.defaultOn);
|
|
217
|
+
first = true;
|
|
218
|
+
}
|
|
219
|
+
const forced = this.options.forced?.() ?? new Set<string>();
|
|
220
|
+
const rootId = this.options.rootId ?? nodeIdOf(this.root);
|
|
221
|
+
const next = new Set<string>();
|
|
222
|
+
items.forEach((item, index) => {
|
|
223
|
+
const key = this.keyOf(item, items);
|
|
224
|
+
const on = this.live.has(key) || forced.has(key);
|
|
225
|
+
if (on) next.add(key);
|
|
226
|
+
this.stampItem(item, key, index, on, rootId, first);
|
|
227
|
+
});
|
|
228
|
+
const was = first ? new Set<string>() : this.shown;
|
|
229
|
+
this.shown = next;
|
|
230
|
+
// On the first stamp an item open by default HAS opened: its `itemOn` listeners attached so far hear it
|
|
231
|
+
// (the builder renders an item's parts, and attaches their interactions, before its root's runtime; the
|
|
232
|
+
// live engine attaches the runtime first and its interactions run an already-open item's at once).
|
|
233
|
+
for (const item of items) {
|
|
234
|
+
const key = this.keyOf(item, items);
|
|
235
|
+
if (next.has(key) && !was.has(key)) dispatch(item, ITEM_ON_EVENT);
|
|
236
|
+
else if (!next.has(key) && was.has(key)) dispatch(item, ITEM_OFF_EVENT);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private stampItem(item: HTMLElement, key: string, index: number, on: boolean, rootId: string, first: boolean): void {
|
|
241
|
+
const ids = accordionDomIds(rootId, key);
|
|
242
|
+
if (!item.hasAttribute(ITEM_KEY_ATTRIBUTE)) item.setAttribute(ITEM_KEY_ATTRIBUTE, key);
|
|
243
|
+
setAttr(item, ON_ATTRIBUTE, on ? '' : null);
|
|
244
|
+
const button = this.own(item, BUTTON_SELECTOR);
|
|
245
|
+
const track = this.own(item, TRACK_SELECTOR);
|
|
246
|
+
const region = this.own(item, REGION_SELECTOR);
|
|
247
|
+
const panel = this.own(item, PANEL_SELECTOR);
|
|
248
|
+
setAttr(button, 'type', 'button');
|
|
249
|
+
setAttr(button, 'id', ids.trigger);
|
|
250
|
+
setAttr(button, 'aria-expanded', on ? 'true' : 'false');
|
|
251
|
+
setAttr(button, 'aria-controls', ids.panel);
|
|
252
|
+
setAttr(button, ON_ATTRIBUTE, on ? '' : null);
|
|
253
|
+
setAttr(track, ON_ATTRIBUTE, on ? '' : null);
|
|
254
|
+
setAttr(panel, ON_ATTRIBUTE, on ? '' : null);
|
|
255
|
+
setAttr(region, 'role', 'region');
|
|
256
|
+
setAttr(region, 'id', ids.panel);
|
|
257
|
+
setAttr(region, 'aria-labelledby', ids.trigger);
|
|
258
|
+
if (!region) return;
|
|
259
|
+
const pending = this.closing.get(key);
|
|
260
|
+
if (on) {
|
|
261
|
+
if (pending) { clearTimeout(pending); this.closing.delete(key); }
|
|
262
|
+
setAttr(region, 'hidden', null);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (region.hasAttribute('hidden') || pending) return;
|
|
266
|
+
// Closing: the track collapses first (its transition), THEN the region is hidden — hiding it at once
|
|
267
|
+
// would collapse it with no motion. At first paint, or under reduced motion, it is hidden at once.
|
|
268
|
+
if (first || this.reduced()) { setAttr(region, 'hidden', UNTIL_FOUND); return; }
|
|
269
|
+
const hide = () => {
|
|
270
|
+
this.closing.delete(key);
|
|
271
|
+
if (!item.hasAttribute(ON_ATTRIBUTE)) setAttr(region, 'hidden', UNTIL_FOUND);
|
|
272
|
+
};
|
|
273
|
+
this.closing.set(key, setTimeout(hide, this.closeDelay(track)));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private closeDelay(track: Element | null): number {
|
|
277
|
+
try {
|
|
278
|
+
const view = track?.ownerDocument?.defaultView;
|
|
279
|
+
const raw = track && view ? view.getComputedStyle(track).transitionDuration : '';
|
|
280
|
+
const ms = Math.max(0, ...String(raw).split(',').map(s => (s.trim().endsWith('ms') ? parseFloat(s) : parseFloat(s) * 1000)).filter(n => !isNaN(n)));
|
|
281
|
+
return Math.min(MAX_CLOSE_MS, ms + 20);
|
|
282
|
+
} catch { return 0; }
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** The element `selector` matches inside `item` that belongs to it, not to an item nested in it. */
|
|
286
|
+
private own(item: HTMLElement, selector: string): HTMLElement | null {
|
|
287
|
+
return Array.from(item.querySelectorAll<HTMLElement>(selector)).find(el => closestAccordionItem(el) === item) ?? null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private buttons(): HTMLElement[] {
|
|
291
|
+
return this.items().map(i => this.own(i, BUTTON_SELECTOR)).filter((b): b is HTMLElement => !!b);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private readonly onClick = (event: Event): void => {
|
|
295
|
+
const button = (event.target as Element | null)?.closest?.(BUTTON_SELECTOR);
|
|
296
|
+
if (!button || closestAccordion(button) !== this.root) return;
|
|
297
|
+
const item = closestAccordionItem(button);
|
|
298
|
+
if (!item) return;
|
|
299
|
+
event.preventDefault();
|
|
300
|
+
this.toggle(this.keyOf(item));
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
private readonly onKeydown = (event: Event): void => {
|
|
304
|
+
const e = event as KeyboardEvent;
|
|
305
|
+
const action = accordionKeyAction(e.key);
|
|
306
|
+
const button = (e.target as Element | null)?.closest?.(BUTTON_SELECTOR);
|
|
307
|
+
if (!action || !button || closestAccordion(button) !== this.root) return;
|
|
308
|
+
const buttons = this.buttons();
|
|
309
|
+
const at = buttons.indexOf(button as HTMLElement);
|
|
310
|
+
const target = buttons[accordionFocusIndex(action, at, buttons.length)];
|
|
311
|
+
if (!target) return;
|
|
312
|
+
e.preventDefault();
|
|
313
|
+
target.focus();
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
private readonly onBeforeMatch = (event: Event): void => {
|
|
317
|
+
const region = (event.target as Element | null)?.closest?.(REGION_SELECTOR);
|
|
318
|
+
const item = closestAccordionItem(region);
|
|
319
|
+
if (!item || closestAccordion(item.parentElement) !== this.root) return;
|
|
320
|
+
this.open(this.keyOf(item));
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function dispatch(el: Element, type: string): void {
|
|
325
|
+
const Ctor = (globalThis as { CustomEvent?: typeof CustomEvent }).CustomEvent;
|
|
326
|
+
if (Ctor) el.dispatchEvent(new Ctor(type, { bubbles: false }));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Attach the runtime to one rendered accordion root, unless it already runs one. Returns the NEW runtime, or
|
|
330
|
+
* null when the root already had one (so a caller destroys only what it created). */
|
|
331
|
+
export function attachAccordionAt(root: HTMLElement, options: Omit<AccordionRuntimeOptions, 'rootId' | 'behaviour'> = {}): AccordionRuntime | null {
|
|
332
|
+
if (ATTACHED.has(root)) return null;
|
|
333
|
+
const runtime = new AccordionRuntime(root, options);
|
|
334
|
+
ATTACHED.set(root, runtime);
|
|
335
|
+
return runtime;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Attach the runtime to every accordion under `container` that has none yet. Returns the new runtimes. */
|
|
339
|
+
export function attachAccordions(container: ParentNode, options: Omit<AccordionRuntimeOptions, 'rootId' | 'behaviour'> = {}): AccordionRuntime[] {
|
|
340
|
+
const out: AccordionRuntime[] = [];
|
|
341
|
+
container.querySelectorAll<HTMLElement>(ROOT_SELECTOR).forEach(root => {
|
|
342
|
+
const runtime = attachAccordionAt(root, options);
|
|
343
|
+
if (runtime) out.push(runtime);
|
|
344
|
+
});
|
|
345
|
+
return out;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Detach a runtime `attachAccordions` created. */
|
|
349
|
+
export function detachAccordion(runtime: AccordionRuntime): void {
|
|
350
|
+
runtime.destroy();
|
|
351
|
+
ATTACHED.delete(runtime.root);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const ATTACHED = new WeakMap<Element, AccordionRuntime>();
|