@vectoriox/iox-ui 5.36.0 → 5.37.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/actions-core/index.d.ts +12 -3
- package/composite-core/index.d.ts +180 -28
- package/fesm2022/vectoriox-iox-ui-actions-core.mjs +26 -2
- package/fesm2022/vectoriox-iox-ui-actions-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-composite-core.mjs +397 -91
- package/fesm2022/vectoriox-iox-ui-composite-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui.mjs +4 -2
- package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
- package/index.d.ts +3 -1
- package/package.json +1 -1
- package/src/actions-core/action.model.ts +4 -2
- package/src/actions-core/element-actions.ts +29 -1
- package/src/composite-core/accordion-runtime.ts +117 -28
- package/src/composite-core/accordion.ts +89 -45
- package/src/composite-core/composite-kinds.ts +110 -0
- package/src/composite-core/composite-node.ts +11 -0
- package/src/composite-core/disclosure.ts +125 -0
- package/src/composite-core/public-api.ts +4 -1
- package/src/composite-core/structure.ts +8 -1
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { readOwnedTrait, writeOwnedTrait, elementStyleById, switchElementStyle, LinkType, setColorScheme, COLOR_SCHEME_TRAIT } from '@vectoriox/iox-ui/design-core';
|
|
2
2
|
|
|
3
|
+
/** A recipe part: a NEW node (`create`) playing `role` under `label`, with the layout `base` styles its recipe
|
|
4
|
+
* gives it — its LOOK is its element style. Shared by every recipe (Accordion, Disclosure). */
|
|
5
|
+
function makeCompositePart(create, spec, role, label, base) {
|
|
6
|
+
const node = create(spec);
|
|
7
|
+
if (role)
|
|
8
|
+
node.role = role;
|
|
9
|
+
if (label)
|
|
10
|
+
node.label = label;
|
|
11
|
+
if (base)
|
|
12
|
+
writeStyles(node, base);
|
|
13
|
+
return node;
|
|
14
|
+
}
|
|
3
15
|
const usesTraits = (node) => Array.isArray(node.traits);
|
|
4
16
|
/** A content trait's value — `traits[].default` on a builder node, `props[name]` on a saved one. */
|
|
5
17
|
function readProp(node, name) {
|
|
@@ -105,6 +117,101 @@ function removeFromComposite(root, node) {
|
|
|
105
117
|
return true;
|
|
106
118
|
}
|
|
107
119
|
|
|
120
|
+
/**
|
|
121
|
+
* The open/close composites' kinds and roles (CC2 Accordion, CC2.1 Disclosure — composite plan T9 / T21). ONE
|
|
122
|
+
* family: a Disclosure is a one-item accordion whose ROOT is its item, so both run the same state machine, the
|
|
123
|
+
* same attribute planners and the same runtime (`accordion-runtime.ts`). Every consumer that treats "an item",
|
|
124
|
+
* "a header" or "a panel" alike asks these predicates, never a role literal.
|
|
125
|
+
*/
|
|
126
|
+
var CompositeKind;
|
|
127
|
+
(function (CompositeKind) {
|
|
128
|
+
CompositeKind["Accordion"] = "accordion";
|
|
129
|
+
CompositeKind["Disclosure"] = "disclosure";
|
|
130
|
+
})(CompositeKind || (CompositeKind = {}));
|
|
131
|
+
var AccordionRole;
|
|
132
|
+
(function (AccordionRole) {
|
|
133
|
+
AccordionRole["Accordion"] = "accordion";
|
|
134
|
+
AccordionRole["Item"] = "accordion.item";
|
|
135
|
+
AccordionRole["Trigger"] = "accordion.trigger";
|
|
136
|
+
AccordionRole["Icon"] = "accordion.icon";
|
|
137
|
+
AccordionRole["Panel"] = "accordion.panel";
|
|
138
|
+
})(AccordionRole || (AccordionRole = {}));
|
|
139
|
+
/**
|
|
140
|
+
* DISCLOSURE ("Read more") — one trigger, one panel. The root IS its item (it carries the item key and
|
|
141
|
+
* `data-iox-on`); the trigger holds two words, the one shown while closed (`more`) and the one shown while open
|
|
142
|
+
* (`less`), swapped by the Item open state — never by script.
|
|
143
|
+
*/
|
|
144
|
+
var DisclosureRole;
|
|
145
|
+
(function (DisclosureRole) {
|
|
146
|
+
DisclosureRole["Disclosure"] = "disclosure";
|
|
147
|
+
DisclosureRole["Trigger"] = "disclosure.trigger";
|
|
148
|
+
DisclosureRole["More"] = "disclosure.more";
|
|
149
|
+
DisclosureRole["Less"] = "disclosure.less";
|
|
150
|
+
DisclosureRole["Icon"] = "disclosure.icon";
|
|
151
|
+
DisclosureRole["Panel"] = "disclosure.panel";
|
|
152
|
+
})(DisclosureRole || (DisclosureRole = {}));
|
|
153
|
+
const COMPOSITE_KIND_ROLES = {
|
|
154
|
+
[CompositeKind.Accordion]: { root: AccordionRole.Accordion, item: AccordionRole.Item, trigger: AccordionRole.Trigger, panel: AccordionRole.Panel, icon: AccordionRole.Icon },
|
|
155
|
+
[CompositeKind.Disclosure]: { root: DisclosureRole.Disclosure, item: DisclosureRole.Disclosure, trigger: DisclosureRole.Trigger, panel: DisclosureRole.Panel, icon: DisclosureRole.Icon },
|
|
156
|
+
};
|
|
157
|
+
const KINDS = Object.values(CompositeKind);
|
|
158
|
+
const rolesOf = (pick) => new Set(KINDS.map(k => COMPOSITE_KIND_ROLES[k][pick]));
|
|
159
|
+
const ROOT_ROLES = rolesOf('root');
|
|
160
|
+
const ITEM_ROLES = rolesOf('item');
|
|
161
|
+
const TRIGGER_ROLES = rolesOf('trigger');
|
|
162
|
+
const PANEL_ROLES = rolesOf('panel');
|
|
163
|
+
/** The kind a role belongs to (`disclosure.panel` → Disclosure), or null for any other role. */
|
|
164
|
+
function compositeKindOfRole(role) {
|
|
165
|
+
const head = role?.split('.')[0];
|
|
166
|
+
return KINDS.includes(head ?? '') ? head : null;
|
|
167
|
+
}
|
|
168
|
+
/** A root that runs the open/close runtime (an Accordion or a Disclosure). */
|
|
169
|
+
function isCompositeRoot(node) {
|
|
170
|
+
return !!node?.role && ROOT_ROLES.has(node.role);
|
|
171
|
+
}
|
|
172
|
+
/** An ITEM — an accordion's item, or a disclosure (its own item). */
|
|
173
|
+
function isCompositeItem(node) {
|
|
174
|
+
return !!node?.role && ITEM_ROLES.has(node.role);
|
|
175
|
+
}
|
|
176
|
+
/** A trigger — rendered as a `<button>` that opens its item, never a link. */
|
|
177
|
+
function isCompositeTrigger(node) {
|
|
178
|
+
return !!node?.role && TRIGGER_ROLES.has(node.role);
|
|
179
|
+
}
|
|
180
|
+
/** A panel — rendered inside the track + region the one hiding rule collapses. */
|
|
181
|
+
function isCompositePanel(node) {
|
|
182
|
+
return !!node?.role && PANEL_ROLES.has(node.role);
|
|
183
|
+
}
|
|
184
|
+
function isDisclosureRoot(node) {
|
|
185
|
+
return node?.role === DisclosureRole.Disclosure;
|
|
186
|
+
}
|
|
187
|
+
/** True for any part of a disclosure (its root included). */
|
|
188
|
+
function isDisclosurePart(node) {
|
|
189
|
+
return compositeKindOfRole(node?.role) === CompositeKind.Disclosure;
|
|
190
|
+
}
|
|
191
|
+
/** The composite root a node belongs to: the nearest ancestor (or the node itself) that is one. `path` = the
|
|
192
|
+
* node's ancestors, root first. */
|
|
193
|
+
function compositeRootOf(node, path) {
|
|
194
|
+
if (isCompositeRoot(node))
|
|
195
|
+
return node;
|
|
196
|
+
for (let i = path.length - 1; i >= 0; i--)
|
|
197
|
+
if (isCompositeRoot(path[i]))
|
|
198
|
+
return path[i];
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
/** The item a node sits in — the nearest item of either kind (itself when it is one) — or null. An accordion
|
|
202
|
+
* root ends the walk (its items are below it). */
|
|
203
|
+
function compositeItemOf(node, path) {
|
|
204
|
+
if (isCompositeItem(node))
|
|
205
|
+
return node;
|
|
206
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
207
|
+
if (isCompositeItem(path[i]))
|
|
208
|
+
return path[i];
|
|
209
|
+
if (isCompositeRoot(path[i]))
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
108
215
|
/**
|
|
109
216
|
* ACCORDION (CC2 — composite K1; complex-components boards 3, 4 and 9, APPROVED 2026-10-20). Not a
|
|
110
217
|
* component: a recipe of ordinary nodes, each part carrying a role, so every part is selectable on the
|
|
@@ -123,14 +230,6 @@ function removeFromComposite(root, node) {
|
|
|
123
230
|
* (`nextAccordionState`), the ARIA and DOM state one planner (`accordionItemState` and the attribute
|
|
124
231
|
* functions), the keyboard one key map — read by the builder canvas, both engine paths and the composer.
|
|
125
232
|
*/
|
|
126
|
-
var AccordionRole;
|
|
127
|
-
(function (AccordionRole) {
|
|
128
|
-
AccordionRole["Accordion"] = "accordion";
|
|
129
|
-
AccordionRole["Item"] = "accordion.item";
|
|
130
|
-
AccordionRole["Trigger"] = "accordion.trigger";
|
|
131
|
-
AccordionRole["Icon"] = "accordion.icon";
|
|
132
|
-
AccordionRole["Panel"] = "accordion.panel";
|
|
133
|
-
})(AccordionRole || (AccordionRole = {}));
|
|
134
233
|
/** The behaviour options a composite root stores as ordinary TRAITS (composite plan §3). */
|
|
135
234
|
var CompositeBehaviourTrait;
|
|
136
235
|
(function (CompositeBehaviourTrait) {
|
|
@@ -172,7 +271,7 @@ const ACCORDION_WORDS = [
|
|
|
172
271
|
{ question: 'Write the third question', answer: 'Answer it plainly, in two or three short sentences.' },
|
|
173
272
|
];
|
|
174
273
|
const ACCORDION_NEW_ITEM_WORDS = { question: 'Write a new question', answer: 'Answer it plainly, in two or three short sentences.' };
|
|
175
|
-
var Type$
|
|
274
|
+
var Type$2;
|
|
176
275
|
(function (Type) {
|
|
177
276
|
Type["Container"] = "Container";
|
|
178
277
|
Type["LinkedContainer"] = "LinkedContainer";
|
|
@@ -180,8 +279,8 @@ var Type$1;
|
|
|
180
279
|
Type["Text"] = "Text";
|
|
181
280
|
Type["Icon"] = "Icon";
|
|
182
281
|
Type["Repeater"] = "Repeater";
|
|
183
|
-
})(Type$
|
|
184
|
-
var Style$
|
|
282
|
+
})(Type$2 || (Type$2 = {}));
|
|
283
|
+
var Style$2;
|
|
185
284
|
(function (Style) {
|
|
186
285
|
Style["Root"] = "box";
|
|
187
286
|
Style["Item"] = "accordion-item";
|
|
@@ -190,14 +289,14 @@ var Style$1;
|
|
|
190
289
|
Style["Icon"] = "icon";
|
|
191
290
|
Style["Panel"] = "accordion-panel";
|
|
192
291
|
Style["Answer"] = "body";
|
|
193
|
-
})(Style$
|
|
194
|
-
const ZERO_BOX$
|
|
292
|
+
})(Style$2 || (Style$2 = {}));
|
|
293
|
+
const ZERO_BOX$2 = '0px 0px 0px 0px';
|
|
195
294
|
const HEADING_LEVELS = ['h2', 'h3', 'h4', 'h5', 'h6'];
|
|
196
295
|
const DEFAULT_LEVEL = 'h3';
|
|
197
296
|
/** A linked trait the header must never carry: it is a button, not a link. */
|
|
198
297
|
const LINK_TYPE$1 = 'linkType';
|
|
199
298
|
/** What each part holds in the recipe — layout only; its LOOK is its element style. */
|
|
200
|
-
const PART_BASE = {
|
|
299
|
+
const PART_BASE$1 = {
|
|
201
300
|
// The items are a column a token apart (`accordion-gap`: 0 for a divider list, 12px for cards).
|
|
202
301
|
[AccordionRole.Accordion]: { display: 'flex', flexDirection: 'column', gap: 'var(--iox-accordion-gap)', width: '100%', minHeight: 'auto', height: 'auto' },
|
|
203
302
|
[AccordionRole.Item]: { display: 'block', width: '100%', minHeight: 'auto', height: 'auto' },
|
|
@@ -208,22 +307,16 @@ const PART_BASE = {
|
|
|
208
307
|
};
|
|
209
308
|
/** The question INHERITS the header's ink (board 4's wears table: the Question reads no colour token), so the
|
|
210
309
|
* header's hover ink reaches the words; and it sits in the row with no margin of its own. */
|
|
211
|
-
const QUESTION_BASE = { color: 'inherit', margin: ZERO_BOX$
|
|
212
|
-
const ANSWER_BASE = { margin: ZERO_BOX$
|
|
213
|
-
function make$
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (role && PART_BASE[role])
|
|
220
|
-
writeStyles(node, PART_BASE[role]);
|
|
221
|
-
return node;
|
|
222
|
-
}
|
|
223
|
-
/** The item's icon; its Item-open state (the turn) is driven by ITS item (`stateTriggers`). */
|
|
224
|
-
function buildIcon(create, key, icon, itemId) {
|
|
310
|
+
const QUESTION_BASE = { color: 'inherit', margin: ZERO_BOX$2, flexGrow: '1', minWidth: '0px' };
|
|
311
|
+
const ANSWER_BASE = { margin: ZERO_BOX$2, padding: ZERO_BOX$2, minHeight: 'auto' };
|
|
312
|
+
function make$2(create, key, type, role, style, props = {}, label) {
|
|
313
|
+
return makeCompositePart(create, { key, type, props, elementStyle: style }, role, label, role ? PART_BASE$1[role] : undefined);
|
|
314
|
+
}
|
|
315
|
+
/** The icon of an item (an accordion's, or a disclosure's — `role`); its Item-open state (the turn) is driven by
|
|
316
|
+
* ITS item (`stateTriggers`). ONE builder for both kinds. */
|
|
317
|
+
function buildCompositeIcon(create, key, icon, itemId, role = AccordionRole.Icon) {
|
|
225
318
|
const glyph = ACCORDION_ICON_GLYPH[icon];
|
|
226
|
-
const node =
|
|
319
|
+
const node = makeCompositePart(create, { key, type: Type$2.Icon, props: { iconClass: glyph.iconClass }, elementStyle: Style$2.Icon }, role, 'Icon', PART_BASE$1[AccordionRole.Icon]);
|
|
227
320
|
node.stateStyles = { ...(node.stateStyles ?? {}), [ITEM_OPEN_STATE]: { rotate: glyph.turn } };
|
|
228
321
|
if (itemId)
|
|
229
322
|
node.stateTriggers = { ...(node.stateTriggers ?? {}), [ITEM_OPEN_STATE]: itemId };
|
|
@@ -231,14 +324,14 @@ function buildIcon(create, key, icon, itemId) {
|
|
|
231
324
|
}
|
|
232
325
|
/** One item: header (question + icon) and panel (answer). `key` makes every part's recipe key unique. */
|
|
233
326
|
function buildAccordionItem(create, words, itemKey, icon = AccordionIcon.Chevron) {
|
|
234
|
-
const item = make$
|
|
327
|
+
const item = make$2(create, `${itemKey}`, Type$2.Container, AccordionRole.Item, Style$2.Item, {}, 'Item');
|
|
235
328
|
item.itemKey = itemKey;
|
|
236
|
-
const trigger = make$
|
|
237
|
-
const question = make$
|
|
329
|
+
const trigger = make$2(create, `${itemKey}-trigger`, Type$2.LinkedContainer, AccordionRole.Trigger, Style$2.Trigger, {}, 'Header');
|
|
330
|
+
const question = make$2(create, `${itemKey}-question`, Type$2.Heading, null, Style$2.Question, { content: words.question, level: DEFAULT_LEVEL }, 'Heading');
|
|
238
331
|
writeStyles(question, QUESTION_BASE);
|
|
239
|
-
trigger.children = [question, ...(icon === AccordionIcon.None ? [] : [
|
|
240
|
-
const panel = make$
|
|
241
|
-
const answer = make$
|
|
332
|
+
trigger.children = [question, ...(icon === AccordionIcon.None ? [] : [buildCompositeIcon(create, `${itemKey}-icon`, icon, item.id)])];
|
|
333
|
+
const panel = make$2(create, `${itemKey}-panel`, Type$2.Container, AccordionRole.Panel, Style$2.Panel, {}, 'Panel');
|
|
334
|
+
const answer = make$2(create, `${itemKey}-answer`, Type$2.Text, null, Style$2.Answer, { content: words.answer, tag: 'p' }, 'Text');
|
|
242
335
|
writeStyles(answer, ANSWER_BASE);
|
|
243
336
|
panel.children = [answer];
|
|
244
337
|
item.children = [trigger, panel];
|
|
@@ -246,7 +339,7 @@ function buildAccordionItem(create, words, itemKey, icon = AccordionIcon.Chevron
|
|
|
246
339
|
}
|
|
247
340
|
/** A fresh Accordion: three items, one open at a time, the first open by default (board 4). */
|
|
248
341
|
function buildAccordionRecipe(create, options = {}) {
|
|
249
|
-
const root = make$
|
|
342
|
+
const root = make$2(create, 'accordion', Type$2.Container, AccordionRole.Accordion, Style$2.Root, {}, 'Accordion');
|
|
250
343
|
writeProp(root, CompositeBehaviourTrait.Multiple, options.multiple ?? false);
|
|
251
344
|
writeProp(root, CompositeBehaviourTrait.DefaultOn, options.defaultOn ?? AccordionDefaultOn.First);
|
|
252
345
|
const words = options.items ?? ACCORDION_WORDS;
|
|
@@ -285,7 +378,7 @@ function accordionItemOf(node, path) {
|
|
|
285
378
|
}
|
|
286
379
|
/** True when the root takes its items from content (a Repeater bound to a list, R3). */
|
|
287
380
|
function isAccordionFromContent(root) {
|
|
288
|
-
return !!root && root.type === Type$
|
|
381
|
+
return !!root && root.type === Type$2.Repeater;
|
|
289
382
|
}
|
|
290
383
|
/** The root's items in order — every item (Manual), or the one template item (From content). */
|
|
291
384
|
function accordionItems(root) {
|
|
@@ -307,29 +400,43 @@ function setAccordionBehaviour(root, patch) {
|
|
|
307
400
|
ok = writeProp(root, CompositeBehaviourTrait.DefaultOn, patch.defaultOn) && ok;
|
|
308
401
|
return ok;
|
|
309
402
|
}
|
|
310
|
-
/** The
|
|
403
|
+
/** The roles of the kind an item belongs to (an accordion item, or a disclosure — its own item). */
|
|
404
|
+
function kindRolesOf(node) {
|
|
405
|
+
return COMPOSITE_KIND_ROLES[compositeKindOfRole(node.role) ?? CompositeKind.Accordion];
|
|
406
|
+
}
|
|
407
|
+
/** The header of an item (either kind). */
|
|
311
408
|
function accordionTriggerOf(item) {
|
|
312
|
-
return findByRole(item,
|
|
409
|
+
return findByRole(item, kindRolesOf(item).trigger);
|
|
313
410
|
}
|
|
314
|
-
/** The panel of an item. */
|
|
411
|
+
/** The panel of an item (either kind). */
|
|
315
412
|
function accordionPanelOf(item) {
|
|
316
|
-
return findByRole(item,
|
|
413
|
+
return findByRole(item, kindRolesOf(item).panel);
|
|
317
414
|
}
|
|
318
|
-
/**
|
|
415
|
+
/** A root's items — an accordion's (`accordionItems`), or a disclosure, which is its own one item. */
|
|
416
|
+
function compositeItems(root) {
|
|
417
|
+
return compositeKindOfRole(root.role) === CompositeKind.Disclosure ? [root] : accordionItems(root);
|
|
418
|
+
}
|
|
419
|
+
/** The question: the first Heading inside the header — a disclosure's toggle holds no heading, so its first word
|
|
420
|
+
* (the Text shown while closed, "Read more"). */
|
|
319
421
|
function accordionQuestionOf(item) {
|
|
320
422
|
const trigger = accordionTriggerOf(item);
|
|
321
423
|
let hit;
|
|
424
|
+
let text;
|
|
322
425
|
if (trigger)
|
|
323
|
-
walkComposite(trigger, n => {
|
|
324
|
-
hit
|
|
325
|
-
|
|
426
|
+
walkComposite(trigger, n => {
|
|
427
|
+
if (!hit && n.type === Type$2.Heading)
|
|
428
|
+
hit = n;
|
|
429
|
+
if (!text && n.type === Type$2.Text)
|
|
430
|
+
text = n;
|
|
431
|
+
});
|
|
432
|
+
return hit ?? text;
|
|
326
433
|
}
|
|
327
434
|
/** The answer: the first Text inside the panel. */
|
|
328
435
|
function accordionAnswerOf(item) {
|
|
329
436
|
const panel = accordionPanelOf(item);
|
|
330
437
|
let hit;
|
|
331
438
|
if (panel)
|
|
332
|
-
walkComposite(panel, n => { if (!hit && n.type === Type$
|
|
439
|
+
walkComposite(panel, n => { if (!hit && n.type === Type$2.Text)
|
|
333
440
|
hit = n; });
|
|
334
441
|
return hit;
|
|
335
442
|
}
|
|
@@ -350,7 +457,7 @@ function accordionHeadingLevel(trigger) {
|
|
|
350
457
|
const visit = (n) => {
|
|
351
458
|
if (level !== undefined)
|
|
352
459
|
return;
|
|
353
|
-
if (n.type === Type$
|
|
460
|
+
if (n.type === Type$2.Heading) {
|
|
354
461
|
level = readProp(n, 'level');
|
|
355
462
|
return;
|
|
356
463
|
}
|
|
@@ -406,10 +513,10 @@ function accordionRemovalRefusal(root, item) {
|
|
|
406
513
|
return null;
|
|
407
514
|
return accordionItems(root).length <= 1 ? AccordionEditRefusal.LastItem : null;
|
|
408
515
|
}
|
|
409
|
-
/** The icon the
|
|
516
|
+
/** The icon the composite's items wear now (the first item's; None when it has no icon). Either kind. */
|
|
410
517
|
function detectAccordionIcon(root) {
|
|
411
|
-
const first =
|
|
412
|
-
const icon = first ? findByRole(first,
|
|
518
|
+
const first = compositeItems(root)[0];
|
|
519
|
+
const icon = first ? findByRole(first, kindRolesOf(root).icon) : undefined;
|
|
413
520
|
if (!icon)
|
|
414
521
|
return first ? AccordionIcon.None : AccordionIcon.Chevron;
|
|
415
522
|
const cls = String(readProp(icon, 'iconClass') ?? '');
|
|
@@ -422,11 +529,12 @@ function detectAccordionIcon(root) {
|
|
|
422
529
|
*/
|
|
423
530
|
function setAccordionIcon(root, icon, create) {
|
|
424
531
|
let changed = 0;
|
|
425
|
-
|
|
532
|
+
const iconRole = kindRolesOf(root).icon;
|
|
533
|
+
for (const item of compositeItems(root)) {
|
|
426
534
|
const trigger = accordionTriggerOf(item);
|
|
427
535
|
if (!trigger)
|
|
428
536
|
continue;
|
|
429
|
-
const existing = findByRole(trigger,
|
|
537
|
+
const existing = findByRole(trigger, iconRole);
|
|
430
538
|
if (icon === AccordionIcon.None) {
|
|
431
539
|
if (existing && removeFromComposite(trigger, existing))
|
|
432
540
|
changed++;
|
|
@@ -439,7 +547,7 @@ function setAccordionIcon(root, icon, create) {
|
|
|
439
547
|
changed++;
|
|
440
548
|
}
|
|
441
549
|
else if (create) {
|
|
442
|
-
trigger.children = [...(trigger.children ?? []),
|
|
550
|
+
trigger.children = [...(trigger.children ?? []), buildCompositeIcon(create, `${item.itemKey ?? 'item'}-icon`, icon, item.id, iconRole)];
|
|
443
551
|
changed++;
|
|
444
552
|
}
|
|
445
553
|
}
|
|
@@ -475,10 +583,16 @@ function nextAccordionState(open, key, behaviour) {
|
|
|
475
583
|
}
|
|
476
584
|
return behaviour.multiple ? new Set([...open, key]) : new Set([key]);
|
|
477
585
|
}
|
|
478
|
-
/** The open keys after OPENING `key` (find-in-page, selecting into a closed panel). */
|
|
586
|
+
/** The open keys after OPENING `key` (find-in-page, selecting into a closed panel, the `openItem` action). */
|
|
479
587
|
function openAccordionItem(open, key, behaviour) {
|
|
480
588
|
return open.has(key) ? new Set(open) : nextAccordionState(open, key, behaviour);
|
|
481
589
|
}
|
|
590
|
+
/** The open keys after CLOSING `key` (the `closeItem` action). */
|
|
591
|
+
function closeAccordionItem(open, key) {
|
|
592
|
+
const next = new Set(open);
|
|
593
|
+
next.delete(key);
|
|
594
|
+
return next;
|
|
595
|
+
}
|
|
482
596
|
var AccordionKeyAction;
|
|
483
597
|
(function (AccordionKeyAction) {
|
|
484
598
|
AccordionKeyAction["Next"] = "next";
|
|
@@ -520,17 +634,30 @@ function rowItemKey(row, index) {
|
|
|
520
634
|
const id = r && typeof r === 'object' ? (r._id ?? r.id) : undefined;
|
|
521
635
|
return id !== undefined && id !== null && id !== '' ? domSafeKey(id) : `row-${index}`;
|
|
522
636
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
637
|
+
const DOM_ID_PREFIX = {
|
|
638
|
+
[CompositeKind.Accordion]: 'iox-acc',
|
|
639
|
+
[CompositeKind.Disclosure]: 'iox-dis',
|
|
640
|
+
};
|
|
641
|
+
/** The ARIA pairing ids of an item: unique per composite (its SCOPE id — `compositeScopeId`) and per item (its key). */
|
|
642
|
+
function accordionDomIds(rootId, itemKey, kind = CompositeKind.Accordion) {
|
|
643
|
+
const base = `${DOM_ID_PREFIX[kind]}-${domSafeKey(rootId)}-${domSafeKey(itemKey)}`;
|
|
526
644
|
return { trigger: `${base}-h`, panel: `${base}-p` };
|
|
527
645
|
}
|
|
528
|
-
|
|
529
|
-
|
|
646
|
+
/**
|
|
647
|
+
* The id a rendered composite's ARIA ids are built from: its node id, and — when it sits inside repeated rows —
|
|
648
|
+
* every enclosing row's identity, outermost first (vectoriox/iox-ui#20: an accordion repeated per card used to
|
|
649
|
+
* pair every row's header with the FIRST row's panel). The engine folds each row's `rowItemKey`; the builder
|
|
650
|
+
* canvas its preview row's index. Unrepeated, it is the node id unchanged.
|
|
651
|
+
*/
|
|
652
|
+
function compositeScopeId(rootId, rowKeys = []) {
|
|
653
|
+
return rowKeys.length ? [rootId, ...rowKeys.map(domSafeKey)].join('-') : rootId;
|
|
654
|
+
}
|
|
655
|
+
function accordionScope(rootId, behaviour, keys, kind = CompositeKind.Accordion) {
|
|
656
|
+
return { kind, rootId, behaviour, keys, open: initialAccordionOpen(keys, behaviour.defaultOn) };
|
|
530
657
|
}
|
|
531
658
|
function accordionItemState(scope, index) {
|
|
532
659
|
const key = scope.keys[index] ?? `item-${index}`;
|
|
533
|
-
return { key, index, on: scope.open.has(key), ids: accordionDomIds(scope.rootId, key) };
|
|
660
|
+
return { kind: scope.kind, key, index, on: scope.open.has(key), ids: accordionDomIds(scope.rootId, key, scope.kind) };
|
|
534
661
|
}
|
|
535
662
|
/** `data-iox-on` — the ONE attribute every "on" style keys on (Open, Item open). */
|
|
536
663
|
const ON_ATTRIBUTE = 'data-iox-on';
|
|
@@ -538,6 +665,9 @@ const ITEM_KEY_ATTRIBUTE = 'data-iox-item-key';
|
|
|
538
665
|
const COMPOSITE_ATTRIBUTE = 'data-iox-composite';
|
|
539
666
|
const MULTIPLE_ATTRIBUTE = 'data-iox-multiple';
|
|
540
667
|
const DEFAULT_ON_ATTRIBUTE = 'data-iox-default-on';
|
|
668
|
+
/** The scope id the ARIA ids are built from (`compositeScopeId`), written by the engine so the runtime re-stamps
|
|
669
|
+
* exactly the ids the server rendered. */
|
|
670
|
+
const SCOPE_ATTRIBUTE = 'data-iox-scope';
|
|
541
671
|
/** The value a closed panel's region carries: find-in-page can still reveal it (D9, board 4). */
|
|
542
672
|
const UNTIL_FOUND = 'until-found';
|
|
543
673
|
/** The structural classes the renderers write around an accordion's parts (no node owns them). */
|
|
@@ -548,11 +678,12 @@ var AccordionClass;
|
|
|
548
678
|
AccordionClass["Track"] = "iox-accordion-track";
|
|
549
679
|
AccordionClass["Region"] = "iox-accordion-region";
|
|
550
680
|
})(AccordionClass || (AccordionClass = {}));
|
|
551
|
-
function accordionRootAttributes(behaviour) {
|
|
681
|
+
function accordionRootAttributes(behaviour, kind = CompositeKind.Accordion, scopeId) {
|
|
552
682
|
return {
|
|
553
|
-
[COMPOSITE_ATTRIBUTE]:
|
|
683
|
+
[COMPOSITE_ATTRIBUTE]: kind,
|
|
554
684
|
[DEFAULT_ON_ATTRIBUTE]: behaviour.defaultOn,
|
|
555
|
-
...(behaviour.multiple ? { [MULTIPLE_ATTRIBUTE]: '' } : {}),
|
|
685
|
+
...(behaviour.multiple && kind === CompositeKind.Accordion ? { [MULTIPLE_ATTRIBUTE]: '' } : {}),
|
|
686
|
+
...(scopeId ? { [SCOPE_ATTRIBUTE]: scopeId } : {}),
|
|
556
687
|
};
|
|
557
688
|
}
|
|
558
689
|
function accordionItemAttributes(state) {
|
|
@@ -571,15 +702,21 @@ function accordionTriggerAttributes(state) {
|
|
|
571
702
|
function accordionTrackAttributes(state) {
|
|
572
703
|
return state.on ? { [ON_ATTRIBUTE]: '' } : {};
|
|
573
704
|
}
|
|
574
|
-
/** The region the panel sits in: `hidden="until-found"` while closed, so its words stay in the page.
|
|
705
|
+
/** The region the panel sits in: `hidden="until-found"` while closed, so its words stay in the page. An accordion's
|
|
706
|
+
* is a labelled `region` (APG accordion); a disclosure's is a plain block (APG disclosure: the button names it). */
|
|
575
707
|
function accordionRegionAttributes(state) {
|
|
708
|
+
const labelled = hasLabelledRegion(state.kind);
|
|
576
709
|
return {
|
|
577
|
-
role: 'region',
|
|
710
|
+
...(labelled ? { role: 'region' } : {}),
|
|
578
711
|
id: state.ids.panel,
|
|
579
|
-
'aria-labelledby': state.ids.trigger,
|
|
712
|
+
...(labelled ? { 'aria-labelledby': state.ids.trigger } : {}),
|
|
580
713
|
...(state.on ? {} : { hidden: UNTIL_FOUND }),
|
|
581
714
|
};
|
|
582
715
|
}
|
|
716
|
+
/** True when a region of this kind is a labelled `role=region`. */
|
|
717
|
+
function hasLabelledRegion(kind) {
|
|
718
|
+
return kind === CompositeKind.Accordion;
|
|
719
|
+
}
|
|
583
720
|
function accordionPanelAttributes(state) {
|
|
584
721
|
return state.on ? { [ON_ATTRIBUTE]: '' } : {};
|
|
585
722
|
}
|
|
@@ -654,13 +791,17 @@ function accordionPartStyle(node, name) {
|
|
|
654
791
|
* paths render the same markup). It reads the markup the renderers write (iox-design.css + accordion.ts's
|
|
655
792
|
* attribute planners), so the server's first paint and every later state agree:
|
|
656
793
|
*
|
|
657
|
-
* [root] data-iox-composite="accordion" data-iox-default-on [data-iox-multiple]
|
|
658
|
-
* its role class may sit on a wrapper host, as the engine's component-path Repeater draws it)
|
|
794
|
+
* [root] data-iox-composite="accordion" data-iox-default-on [data-iox-multiple] [data-iox-scope] (the node's
|
|
795
|
+
* own element — its role class may sit on a wrapper host, as the engine's component-path Repeater draws it)
|
|
659
796
|
* .iox-role-accordion-item data-iox-item-key [data-iox-on]
|
|
660
797
|
* hN.iox-accordion-heading > button.iox-accordion-button id aria-expanded aria-controls
|
|
661
798
|
* .iox-accordion-track [data-iox-on] > .iox-accordion-region role=region [hidden=until-found]
|
|
662
799
|
* .iox-role-accordion-panel
|
|
663
800
|
*
|
|
801
|
+
* The DISCLOSURE (CC2.1) runs on this same runtime: `data-iox-composite="disclosure"` on a root that IS its one
|
|
802
|
+
* item (it carries the key and `data-iox-on`), its button straight inside it (no heading), its panel in the same
|
|
803
|
+
* track + region (a plain block — no `role=region`), and no arrow keys (one button).
|
|
804
|
+
*
|
|
664
805
|
* It owns: a click on a header toggles its item; ↓ ↑ Home End move between the headers of THIS accordion
|
|
665
806
|
* (a nested one keeps its own); find-in-page (`beforematch`) opens the item that holds the match; a closed
|
|
666
807
|
* panel gets `hidden="until-found"` after its track has collapsed (at once under reduced motion). Every
|
|
@@ -670,23 +811,31 @@ function accordionPartStyle(node, name) {
|
|
|
670
811
|
*/
|
|
671
812
|
const ITEM_ON_EVENT = 'iox-item-on';
|
|
672
813
|
const ITEM_OFF_EVENT = 'iox-item-off';
|
|
673
|
-
const
|
|
674
|
-
const
|
|
675
|
-
|
|
814
|
+
const KIND_ROOT = (kind) => `[${COMPOSITE_ATTRIBUTE}="${kind}"]`;
|
|
815
|
+
const ROOT_SELECTOR = Object.values(CompositeKind).map(KIND_ROOT).join(', ');
|
|
816
|
+
/** An accordion's items are its `.iox-role-accordion-item` children; a disclosure is its own item. */
|
|
817
|
+
const ACCORDION_ITEM_SELECTOR = '.iox-role-accordion-item';
|
|
818
|
+
const ITEM_SELECTOR = `${ACCORDION_ITEM_SELECTOR}, ${KIND_ROOT(CompositeKind.Disclosure)}`;
|
|
819
|
+
const PANEL_SELECTOR = '.iox-role-accordion-panel, .iox-role-disclosure-panel';
|
|
676
820
|
const BUTTON_SELECTOR = `.${AccordionClass.Button}`;
|
|
677
821
|
const TRACK_SELECTOR = `.${AccordionClass.Track}`;
|
|
678
822
|
const REGION_SELECTOR = `.${AccordionClass.Region}`;
|
|
679
823
|
/** How long a closing track may take before its region is hidden (dur-base is 200–320ms in every pack). */
|
|
680
824
|
const MAX_CLOSE_MS = 600;
|
|
681
825
|
const REDUCED_MOTION = '(prefers-reduced-motion: reduce)';
|
|
682
|
-
/** The accordion an element belongs to (the element itself when it is a root). */
|
|
826
|
+
/** The composite (accordion or disclosure) an element belongs to (the element itself when it is a root). */
|
|
683
827
|
function closestAccordion(el) {
|
|
684
828
|
return el?.closest?.(ROOT_SELECTOR) ?? null;
|
|
685
829
|
}
|
|
686
|
-
/** The item an element sits in (itself when it is one). */
|
|
830
|
+
/** The item an element sits in (itself when it is one) — an accordion item, or a disclosure. */
|
|
687
831
|
function closestAccordionItem(el) {
|
|
688
832
|
return el?.closest?.(ITEM_SELECTOR) ?? null;
|
|
689
833
|
}
|
|
834
|
+
const kindOf = (root) => root.getAttribute(COMPOSITE_ATTRIBUTE) === CompositeKind.Disclosure ? CompositeKind.Disclosure : CompositeKind.Accordion;
|
|
835
|
+
/** The composite root whose items an item belongs to: a disclosure is its own root. */
|
|
836
|
+
function rootOfItem(item) {
|
|
837
|
+
return kindOf(item) === CompositeKind.Disclosure && item.matches(KIND_ROOT(CompositeKind.Disclosure)) ? item : closestAccordion(item.parentElement);
|
|
838
|
+
}
|
|
690
839
|
/** Subscribe to an item turning on / off — what the `itemOn` / `itemOff` interaction triggers listen to. The
|
|
691
840
|
* item is the element's own item (the element itself when it is one). Returns the unsubscribe. */
|
|
692
841
|
function onAccordionItemState(el, on, handler) {
|
|
@@ -745,6 +894,9 @@ class AccordionRuntime {
|
|
|
745
894
|
this.toggle(this.keyOf(item));
|
|
746
895
|
};
|
|
747
896
|
this.onKeydown = (event) => {
|
|
897
|
+
// A disclosure has one button: Enter / Space are the button's own, and there is nowhere to move.
|
|
898
|
+
if (this.kind !== CompositeKind.Accordion)
|
|
899
|
+
return;
|
|
748
900
|
const e = event;
|
|
749
901
|
const action = accordionKeyAction(e.key);
|
|
750
902
|
const button = e.target?.closest?.(BUTTON_SELECTOR);
|
|
@@ -761,12 +913,15 @@ class AccordionRuntime {
|
|
|
761
913
|
this.onBeforeMatch = (event) => {
|
|
762
914
|
const region = event.target?.closest?.(REGION_SELECTOR);
|
|
763
915
|
const item = closestAccordionItem(region);
|
|
764
|
-
|
|
916
|
+
const items = this.items();
|
|
917
|
+
if (!item || !items.includes(item))
|
|
765
918
|
return;
|
|
766
|
-
this.open(this.keyOf(item));
|
|
919
|
+
this.open(this.keyOf(item, items));
|
|
767
920
|
};
|
|
921
|
+
this.kind = options.kind ?? kindOf(root);
|
|
768
922
|
this.behaviourValue = options.behaviour ?? behaviourOf(root);
|
|
769
|
-
root.setAttribute(COMPOSITE_ATTRIBUTE,
|
|
923
|
+
root.setAttribute(COMPOSITE_ATTRIBUTE, this.kind);
|
|
924
|
+
RUNTIMES.set(root, this);
|
|
770
925
|
// The server rendered the first state; with no server state the root's default decides.
|
|
771
926
|
// (The builder passes the behaviour and starts from the saved default; on the live site the server
|
|
772
927
|
// stamped every item's key and state, and that is the truth.)
|
|
@@ -805,9 +960,11 @@ class AccordionRuntime {
|
|
|
805
960
|
else
|
|
806
961
|
this.stamp();
|
|
807
962
|
}
|
|
808
|
-
/** THIS
|
|
963
|
+
/** THIS composite's items, in document order (a nested composite's items are its own); a disclosure's is itself. */
|
|
809
964
|
items() {
|
|
810
|
-
|
|
965
|
+
if (this.kind === CompositeKind.Disclosure)
|
|
966
|
+
return [this.root];
|
|
967
|
+
return Array.from(this.root.querySelectorAll(ACCORDION_ITEM_SELECTOR)).filter(i => closestAccordion(i.parentElement) === this.root);
|
|
811
968
|
}
|
|
812
969
|
/** The open keys the live state holds (before anything forced). */
|
|
813
970
|
get liveKeys() {
|
|
@@ -828,6 +985,20 @@ class AccordionRuntime {
|
|
|
828
985
|
this.live = openAccordionItem(this.live, key, this.behaviourValue);
|
|
829
986
|
this.stamp();
|
|
830
987
|
}
|
|
988
|
+
close(key) {
|
|
989
|
+
this.live = closeAccordionItem(this.live, key);
|
|
990
|
+
this.stamp();
|
|
991
|
+
}
|
|
992
|
+
/** Run an item action (`openItem` / `closeItem` / `toggleItem`, T14) on `key`. True when it shows open after. */
|
|
993
|
+
run(action, key) {
|
|
994
|
+
if (action === CompositeItemAction.Open)
|
|
995
|
+
this.open(key);
|
|
996
|
+
else if (action === CompositeItemAction.Close)
|
|
997
|
+
this.close(key);
|
|
998
|
+
else
|
|
999
|
+
this.toggle(key);
|
|
1000
|
+
return this.shown.has(key);
|
|
1001
|
+
}
|
|
831
1002
|
/** Back to the saved default ("Showing on canvas ▸ As saved"). */
|
|
832
1003
|
resetLive() {
|
|
833
1004
|
const items = this.items();
|
|
@@ -839,6 +1010,8 @@ class AccordionRuntime {
|
|
|
839
1010
|
this.stamp();
|
|
840
1011
|
}
|
|
841
1012
|
destroy() {
|
|
1013
|
+
if (RUNTIMES.get(this.root) === this)
|
|
1014
|
+
RUNTIMES.delete(this.root);
|
|
842
1015
|
this.root.removeEventListener('click', this.onClick, true);
|
|
843
1016
|
this.root.removeEventListener('keydown', this.onKeydown);
|
|
844
1017
|
this.root.removeEventListener('beforematch', this.onBeforeMatch, true);
|
|
@@ -878,7 +1051,8 @@ class AccordionRuntime {
|
|
|
878
1051
|
first = true;
|
|
879
1052
|
}
|
|
880
1053
|
const forced = this.options.forced?.() ?? new Set();
|
|
881
|
-
const
|
|
1054
|
+
const own = this.options.rootId;
|
|
1055
|
+
const rootId = (typeof own === 'function' ? own() : own) ?? this.root.getAttribute(SCOPE_ATTRIBUTE) ?? nodeIdOf(this.root);
|
|
882
1056
|
const next = new Set();
|
|
883
1057
|
items.forEach((item, index) => {
|
|
884
1058
|
const key = this.keyOf(item, items);
|
|
@@ -901,7 +1075,7 @@ class AccordionRuntime {
|
|
|
901
1075
|
}
|
|
902
1076
|
}
|
|
903
1077
|
stampItem(item, key, index, on, rootId, first) {
|
|
904
|
-
const ids = accordionDomIds(rootId, key);
|
|
1078
|
+
const ids = accordionDomIds(rootId, key, this.kind);
|
|
905
1079
|
if (!item.hasAttribute(ITEM_KEY_ATTRIBUTE))
|
|
906
1080
|
item.setAttribute(ITEM_KEY_ATTRIBUTE, key);
|
|
907
1081
|
setAttr(item, ON_ATTRIBUTE, on ? '' : null);
|
|
@@ -916,9 +1090,10 @@ class AccordionRuntime {
|
|
|
916
1090
|
setAttr(button, ON_ATTRIBUTE, on ? '' : null);
|
|
917
1091
|
setAttr(track, ON_ATTRIBUTE, on ? '' : null);
|
|
918
1092
|
setAttr(panel, ON_ATTRIBUTE, on ? '' : null);
|
|
919
|
-
|
|
1093
|
+
const labelled = hasLabelledRegion(this.kind);
|
|
1094
|
+
setAttr(region, 'role', labelled ? 'region' : null);
|
|
920
1095
|
setAttr(region, 'id', ids.panel);
|
|
921
|
-
setAttr(region, 'aria-labelledby', ids.trigger);
|
|
1096
|
+
setAttr(region, 'aria-labelledby', labelled ? ids.trigger : null);
|
|
922
1097
|
if (!region)
|
|
923
1098
|
return;
|
|
924
1099
|
const pending = this.closing.get(key);
|
|
@@ -969,14 +1144,12 @@ function dispatch(el, type) {
|
|
|
969
1144
|
if (Ctor)
|
|
970
1145
|
el.dispatchEvent(new Ctor(type, { bubbles: false }));
|
|
971
1146
|
}
|
|
972
|
-
/** Attach the runtime to one rendered
|
|
1147
|
+
/** Attach the runtime to one rendered composite root, unless it already runs one. Returns the NEW runtime, or
|
|
973
1148
|
* null when the root already had one (so a caller destroys only what it created). */
|
|
974
1149
|
function attachAccordionAt(root, options = {}) {
|
|
975
|
-
if (
|
|
1150
|
+
if (RUNTIMES.has(root))
|
|
976
1151
|
return null;
|
|
977
|
-
|
|
978
|
-
ATTACHED.set(root, runtime);
|
|
979
|
-
return runtime;
|
|
1152
|
+
return new AccordionRuntime(root, options);
|
|
980
1153
|
}
|
|
981
1154
|
/** Attach the runtime to every accordion under `container` that has none yet. Returns the new runtimes. */
|
|
982
1155
|
function attachAccordions(container, options = {}) {
|
|
@@ -991,9 +1164,134 @@ function attachAccordions(container, options = {}) {
|
|
|
991
1164
|
/** Detach a runtime `attachAccordions` created. */
|
|
992
1165
|
function detachAccordion(runtime) {
|
|
993
1166
|
runtime.destroy();
|
|
994
|
-
ATTACHED.delete(runtime.root);
|
|
995
1167
|
}
|
|
996
|
-
|
|
1168
|
+
/** Every live runtime by its root — the canvas's and the site's alike (a runtime registers itself and leaves on
|
|
1169
|
+
* destroy), so an item action finds the runtime of whatever item it targets. */
|
|
1170
|
+
const RUNTIMES = new WeakMap();
|
|
1171
|
+
const ROW_INDEX_ATTRIBUTE = 'data-iox-row-idx';
|
|
1172
|
+
/** The repeated rows an element sits in, outermost first — each row's index as its row wrappers stamp it
|
|
1173
|
+
* (`data-iox-row-idx`: the builder's Repeater rows, the engine's component path). The builder folds them into a
|
|
1174
|
+
* composite's scope id (`compositeScopeId`, vectoriox/iox-ui#20); the element itself is not counted. */
|
|
1175
|
+
function compositeRowKeys(el) {
|
|
1176
|
+
const keys = [];
|
|
1177
|
+
for (let at = el?.parentElement?.closest?.(`[${ROW_INDEX_ATTRIBUTE}]`); at; at = at.parentElement?.closest(`[${ROW_INDEX_ATTRIBUTE}]`)) {
|
|
1178
|
+
keys.unshift(at.getAttribute(ROW_INDEX_ATTRIBUTE) ?? '');
|
|
1179
|
+
}
|
|
1180
|
+
return keys;
|
|
1181
|
+
}
|
|
1182
|
+
/** The runtime running a composite root, or null. */
|
|
1183
|
+
function accordionRuntimeOf(root) {
|
|
1184
|
+
return (root && RUNTIMES.get(root)) || null;
|
|
1185
|
+
}
|
|
1186
|
+
// ── T14: open / close / toggle an item from anywhere ─────────────────────────────────────────────
|
|
1187
|
+
/** What an item action does to its target item (actions-core `openItem` / `closeItem` / `toggleItem`). */
|
|
1188
|
+
var CompositeItemAction;
|
|
1189
|
+
(function (CompositeItemAction) {
|
|
1190
|
+
CompositeItemAction["Open"] = "open";
|
|
1191
|
+
CompositeItemAction["Close"] = "close";
|
|
1192
|
+
CompositeItemAction["Toggle"] = "toggle";
|
|
1193
|
+
})(CompositeItemAction || (CompositeItemAction = {}));
|
|
1194
|
+
/**
|
|
1195
|
+
* Run an item action on the element of an ITEM (an accordion item, or a disclosure) — the `openItem` /
|
|
1196
|
+
* `closeItem` / `toggleItem` interaction actions, on the canvas and on the live site through this one function.
|
|
1197
|
+
* Returns whether the item shows open afterwards, or null when `el` is in no running composite.
|
|
1198
|
+
*/
|
|
1199
|
+
function runCompositeItemAction(el, action) {
|
|
1200
|
+
const item = closestAccordionItem(el);
|
|
1201
|
+
const runtime = item ? accordionRuntimeOf(rootOfItem(item)) : null;
|
|
1202
|
+
if (!item || !runtime)
|
|
1203
|
+
return null;
|
|
1204
|
+
return runtime.run(action, runtime.keyOf(item));
|
|
1205
|
+
}
|
|
1206
|
+
/** How much of an opened item must already be on screen for the action NOT to scroll to it (its header row). */
|
|
1207
|
+
const VISIBLE_HEAD_PX = 48;
|
|
1208
|
+
/** True when an item an action just opened is out of view and the page should scroll to it (T14: "opens that item
|
|
1209
|
+
* and scrolls to it") — never when its head is already on screen, so a button beside it does not jolt the page. */
|
|
1210
|
+
function shouldScrollToItem(top, viewportHeight) {
|
|
1211
|
+
return top < 0 || top + VISIBLE_HEAD_PX > viewportHeight;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/** The words a fresh disclosure starts with — generic, the author's to replace. */
|
|
1215
|
+
const DISCLOSURE_WORDS = {
|
|
1216
|
+
more: 'Read more',
|
|
1217
|
+
less: 'Read less',
|
|
1218
|
+
body: 'Write the rest of the story here. It stays out of the way until someone asks for it.',
|
|
1219
|
+
};
|
|
1220
|
+
/** A disclosure's one item key — its root IS its item. */
|
|
1221
|
+
const DISCLOSURE_ITEM_KEY = 'k1';
|
|
1222
|
+
var Type$1;
|
|
1223
|
+
(function (Type) {
|
|
1224
|
+
Type["Container"] = "Container";
|
|
1225
|
+
Type["LinkedContainer"] = "LinkedContainer";
|
|
1226
|
+
Type["Text"] = "Text";
|
|
1227
|
+
})(Type$1 || (Type$1 = {}));
|
|
1228
|
+
var Style$1;
|
|
1229
|
+
(function (Style) {
|
|
1230
|
+
Style["Root"] = "box";
|
|
1231
|
+
Style["Trigger"] = "accordion-trigger";
|
|
1232
|
+
Style["Word"] = "body";
|
|
1233
|
+
Style["Panel"] = "accordion-panel";
|
|
1234
|
+
Style["Body"] = "body";
|
|
1235
|
+
})(Style$1 || (Style$1 = {}));
|
|
1236
|
+
const ZERO_BOX$1 = '0px 0px 0px 0px';
|
|
1237
|
+
const HIDDEN = 'none';
|
|
1238
|
+
const INHERIT = 'inherit';
|
|
1239
|
+
/** A word is a flex item of the toggle — `block` (never `inline`: the compiler drops a value equal to the CSS initial,
|
|
1240
|
+
* and the Text's own default would come back in its place). */
|
|
1241
|
+
const SHOWN = 'block';
|
|
1242
|
+
/** What each part holds — layout only; its LOOK is its element style. The trigger sizes to its words (a button in
|
|
1243
|
+
* the flow, not a full-width header row — `fit-content` keeps it from stretching) and the panel spans the root. The
|
|
1244
|
+
* trigger is `flex`, not `inline-flex`: it is a flex item of the root, which blockifies it on the site anyway. */
|
|
1245
|
+
const PART_BASE = {
|
|
1246
|
+
[DisclosureRole.Disclosure]: { display: 'flex', flexDirection: 'column', width: '100%', minHeight: 'auto', height: 'auto' },
|
|
1247
|
+
// The toggle reads the text around it (the page's body type, through the root) — a LinkedContainer is born with
|
|
1248
|
+
// its own literal font, which an accordion's heading masks and a disclosure's words would show.
|
|
1249
|
+
[DisclosureRole.Trigger]: {
|
|
1250
|
+
display: 'flex', alignItems: 'center', gap: '8px', width: 'fit-content', minHeight: 'auto', height: 'auto', textAlign: 'start', cursor: 'pointer',
|
|
1251
|
+
fontFamily: INHERIT, fontSize: INHERIT, fontWeight: INHERIT, lineHeight: INHERIT,
|
|
1252
|
+
},
|
|
1253
|
+
// A word is just its words: none of a Text's own box (its type's 10px padding, its full width) — the toggle's
|
|
1254
|
+
// padding is the click target.
|
|
1255
|
+
[DisclosureRole.More]: { display: SHOWN, margin: ZERO_BOX$1, padding: ZERO_BOX$1, width: 'auto' },
|
|
1256
|
+
[DisclosureRole.Less]: { display: HIDDEN, margin: ZERO_BOX$1, padding: ZERO_BOX$1, width: 'auto' },
|
|
1257
|
+
[DisclosureRole.Panel]: { display: 'block', width: '100%', minHeight: 'auto', height: 'auto' },
|
|
1258
|
+
};
|
|
1259
|
+
const BODY_BASE = { margin: ZERO_BOX$1, padding: ZERO_BOX$1, minHeight: 'auto' };
|
|
1260
|
+
function make$1(create, key, type, role, style, props, label) {
|
|
1261
|
+
return makeCompositePart(create, { key, type, props, elementStyle: style }, role, label, role ? PART_BASE[role] : undefined);
|
|
1262
|
+
}
|
|
1263
|
+
/** One of the trigger's two words: shown while the disclosure is closed (`more`) or open (`less`). */
|
|
1264
|
+
function buildWord(create, role, words, rootId) {
|
|
1265
|
+
const node = make$1(create, role === DisclosureRole.More ? 'more' : 'less', Type$1.Text, role, Style$1.Word, { content: words, tag: 'span' }, 'Text');
|
|
1266
|
+
node.stateStyles = { ...(node.stateStyles ?? {}), [ITEM_OPEN_STATE]: { display: role === DisclosureRole.More ? HIDDEN : SHOWN } };
|
|
1267
|
+
if (rootId)
|
|
1268
|
+
node.stateTriggers = { ...(node.stateTriggers ?? {}), [ITEM_OPEN_STATE]: rootId };
|
|
1269
|
+
return node;
|
|
1270
|
+
}
|
|
1271
|
+
/** A fresh Disclosure: "Read more" opens the text below and reads "Read less" while it is open; closed by default. */
|
|
1272
|
+
function buildDisclosureRecipe(create, options = {}) {
|
|
1273
|
+
const words = { ...DISCLOSURE_WORDS, ...(options.words ?? {}) };
|
|
1274
|
+
const root = make$1(create, 'disclosure', Type$1.Container, DisclosureRole.Disclosure, Style$1.Root, {}, 'Disclosure');
|
|
1275
|
+
root.itemKey = DISCLOSURE_ITEM_KEY;
|
|
1276
|
+
writeProp(root, CompositeBehaviourTrait.DefaultOn, options.open ? AccordionDefaultOn.First : AccordionDefaultOn.None);
|
|
1277
|
+
const icon = options.icon ?? AccordionIcon.Chevron;
|
|
1278
|
+
const trigger = make$1(create, 'trigger', Type$1.LinkedContainer, DisclosureRole.Trigger, Style$1.Trigger, {}, 'Toggle');
|
|
1279
|
+
trigger.children = [
|
|
1280
|
+
buildWord(create, DisclosureRole.More, words.more, root.id),
|
|
1281
|
+
buildWord(create, DisclosureRole.Less, words.less, root.id),
|
|
1282
|
+
...(icon === AccordionIcon.None ? [] : [buildCompositeIcon(create, 'icon', icon, root.id, DisclosureRole.Icon)]),
|
|
1283
|
+
];
|
|
1284
|
+
const panel = make$1(create, 'panel', Type$1.Container, DisclosureRole.Panel, Style$1.Panel, {}, 'Panel');
|
|
1285
|
+
const body = make$1(create, 'body', Type$1.Text, null, Style$1.Body, { content: words.body, tag: 'p' }, 'Text');
|
|
1286
|
+
writeStyles(body, BODY_BASE);
|
|
1287
|
+
panel.children = [body];
|
|
1288
|
+
root.children = [trigger, panel];
|
|
1289
|
+
return root;
|
|
1290
|
+
}
|
|
1291
|
+
/** Behaviour ▸ Open by default (a disclosure: on or off). */
|
|
1292
|
+
function disclosureOpenByDefault(root) {
|
|
1293
|
+
return accordionBehaviour(root).defaultOn !== AccordionDefaultOn.None;
|
|
1294
|
+
}
|
|
997
1295
|
|
|
998
1296
|
/**
|
|
999
1297
|
* BADGE + CHIP (composite T25; complex-components board 8). A Badge is a Container wearing
|
|
@@ -1552,6 +1850,13 @@ const COMPOSITE_ROLE_SPECS = {
|
|
|
1552
1850
|
[AccordionRole.Trigger]: { parent: AccordionRole.Item, locked: true },
|
|
1553
1851
|
[AccordionRole.Panel]: { parent: AccordionRole.Item, locked: true },
|
|
1554
1852
|
[AccordionRole.Icon]: { parent: AccordionRole.Trigger },
|
|
1853
|
+
// CC2.1 — a disclosure always keeps its toggle and its panel; the words and the icon are the author's.
|
|
1854
|
+
[DisclosureRole.Disclosure]: { parent: null },
|
|
1855
|
+
[DisclosureRole.Trigger]: { parent: DisclosureRole.Disclosure, locked: true },
|
|
1856
|
+
[DisclosureRole.Panel]: { parent: DisclosureRole.Disclosure, locked: true },
|
|
1857
|
+
[DisclosureRole.More]: { parent: DisclosureRole.Trigger },
|
|
1858
|
+
[DisclosureRole.Less]: { parent: DisclosureRole.Trigger },
|
|
1859
|
+
[DisclosureRole.Icon]: { parent: DisclosureRole.Trigger },
|
|
1555
1860
|
};
|
|
1556
1861
|
var StructureEdit;
|
|
1557
1862
|
(function (StructureEdit) {
|
|
@@ -1590,7 +1895,8 @@ function isLockedPart(node) {
|
|
|
1590
1895
|
* plan §3a). FRAMEWORK-FREE: the builder, the SSR engine and iox-cms (generation-core's composer, the site
|
|
1591
1896
|
* build) read the same recipes, roles and structure rules. Card v2 + Badge / Chip (T23 / T25) are the
|
|
1592
1897
|
* first; the Accordion (CC2) adds the first composite RUNTIME (accordion-runtime.ts — DOM only, no
|
|
1593
|
-
* framework); Tabs join with
|
|
1898
|
+
* framework), which the Disclosure (CC2.1) runs too — a one-item accordion whose root is its item; Tabs join with
|
|
1899
|
+
* their own phase.
|
|
1594
1900
|
* ⚠️ Consumers wire the usual trio — ng-package raw-ts assets, jest moduleNameMapper, tsconfig paths.
|
|
1595
1901
|
*/
|
|
1596
1902
|
|
|
@@ -1598,5 +1904,5 @@ function isLockedPart(node) {
|
|
|
1598
1904
|
* Generated bundle index. Do not edit.
|
|
1599
1905
|
*/
|
|
1600
1906
|
|
|
1601
|
-
export { ACCORDION_BIND_PROP, ACCORDION_CONTENT_LIMIT, ACCORDION_DEFAULT_ONS, ACCORDION_ICONS, ACCORDION_ICON_GLYPH, ACCORDION_NEW_ITEM_WORDS, ACCORDION_WORDS, AccordionClass, AccordionDefaultOn, AccordionEditRefusal, AccordionIcon, AccordionKeyAction, AccordionRole, AccordionRuntime, BADGE_TONES, BADGE_TONE_ICONS, BadgeTone, CARD_LAYOUTS, CARD_LAYOUT_CUSTOM, CARD_PARTS, CARD_PART_ROLE, CARD_STACK_BREAKPOINT, CARD_WORDS, CHIP_ICON, CHIP_STYLE, COMPOSITE_ATTRIBUTE, COMPOSITE_ROLE_SPECS, CardLayout, CardPart, CardRole, CompositeBehaviourTrait, DEFAULT_ON_ATTRIBUTE, ITEM_KEY_ATTRIBUTE, ITEM_OFF_EVENT, ITEM_ON_EVENT, ITEM_OPEN_STATE, MULTIPLE_ATTRIBUTE, ON_ATTRIBUTE, ON_STATE, PILL_LABEL_STYLE, StructureEdit, StructureRefusal, UNTIL_FOUND, accordionAnswerOf, accordionBehaviour, accordionDomIds, accordionFocusIndex, accordionHeadingLevel, accordionItemAttributes, accordionItemLabel, accordionItemOf, accordionItemState, accordionItems, accordionKeyAction, accordionPanelAttributes, accordionPanelOf, accordionPartStyle, accordionQuestionOf, accordionRegionAttributes, accordionRemovalRefusal, accordionRootAttributes, accordionRootOf, accordionScope, accordionToContent, accordionToManual, accordionTrackAttributes, accordionTriggerAttributes, accordionTriggerOf, addAccordionItem, applyCardLayout, attachAccordionAt, attachAccordions, badgeStyleId, badgeToneOf, buildAccordionItem, buildAccordionRecipe, buildBadge, buildCardRecipe, buildChip, cardPartsOf, cardRootOf, cardTitleOf, clearLayerStyles, closestAccordion, closestAccordionItem, detachAccordion, detectAccordionIcon, detectCardLayout, domSafeKey, findByRole, initialAccordionOpen, isAccordionFromContent, isAccordionItemOn, isAccordionPart, isAccordionRoot, isCardLinked, isCardRoot, isLockedPart, isPillNode, itemOpenTriggerIds, moveAccordionItem, nextAccordionState, nextItemKey, onAccordionItemState, openAccordionItem, parentInComposite, pickAccordionAutoBinds, readLayerStyle, readProp, readStyle, removeFromComposite, renameAccordionItem, rowItemKey, serializeDomAttributes, setAccordionBehaviour, setAccordionIcon, setBadgeTone, setCardLinked, setCardPart, structureRefusal, walkComposite, writeLayerStyles, writeProp, writeStyles };
|
|
1907
|
+
export { ACCORDION_BIND_PROP, ACCORDION_CONTENT_LIMIT, ACCORDION_DEFAULT_ONS, ACCORDION_ICONS, ACCORDION_ICON_GLYPH, ACCORDION_NEW_ITEM_WORDS, ACCORDION_WORDS, AccordionClass, AccordionDefaultOn, AccordionEditRefusal, AccordionIcon, AccordionKeyAction, AccordionRole, AccordionRuntime, BADGE_TONES, BADGE_TONE_ICONS, BadgeTone, CARD_LAYOUTS, CARD_LAYOUT_CUSTOM, CARD_PARTS, CARD_PART_ROLE, CARD_STACK_BREAKPOINT, CARD_WORDS, CHIP_ICON, CHIP_STYLE, COMPOSITE_ATTRIBUTE, COMPOSITE_KIND_ROLES, COMPOSITE_ROLE_SPECS, CardLayout, CardPart, CardRole, CompositeBehaviourTrait, CompositeItemAction, CompositeKind, DEFAULT_ON_ATTRIBUTE, DISCLOSURE_ITEM_KEY, DISCLOSURE_WORDS, DisclosureRole, ITEM_KEY_ATTRIBUTE, ITEM_OFF_EVENT, ITEM_ON_EVENT, ITEM_OPEN_STATE, MULTIPLE_ATTRIBUTE, ON_ATTRIBUTE, ON_STATE, PILL_LABEL_STYLE, SCOPE_ATTRIBUTE, StructureEdit, StructureRefusal, UNTIL_FOUND, accordionAnswerOf, accordionBehaviour, accordionDomIds, accordionFocusIndex, accordionHeadingLevel, accordionItemAttributes, accordionItemLabel, accordionItemOf, accordionItemState, accordionItems, accordionKeyAction, accordionPanelAttributes, accordionPanelOf, accordionPartStyle, accordionQuestionOf, accordionRegionAttributes, accordionRemovalRefusal, accordionRootAttributes, accordionRootOf, accordionRuntimeOf, accordionScope, accordionToContent, accordionToManual, accordionTrackAttributes, accordionTriggerAttributes, accordionTriggerOf, addAccordionItem, applyCardLayout, attachAccordionAt, attachAccordions, badgeStyleId, badgeToneOf, buildAccordionItem, buildAccordionRecipe, buildBadge, buildCardRecipe, buildChip, buildCompositeIcon, buildDisclosureRecipe, cardPartsOf, cardRootOf, cardTitleOf, clearLayerStyles, closeAccordionItem, closestAccordion, closestAccordionItem, compositeItemOf, compositeItems, compositeKindOfRole, compositeRootOf, compositeRowKeys, compositeScopeId, detachAccordion, detectAccordionIcon, detectCardLayout, disclosureOpenByDefault, domSafeKey, findByRole, hasLabelledRegion, initialAccordionOpen, isAccordionFromContent, isAccordionItemOn, isAccordionPart, isAccordionRoot, isCardLinked, isCardRoot, isCompositeItem, isCompositePanel, isCompositeRoot, isCompositeTrigger, isDisclosurePart, isDisclosureRoot, isLockedPart, isPillNode, itemOpenTriggerIds, makeCompositePart, moveAccordionItem, nextAccordionState, nextItemKey, onAccordionItemState, openAccordionItem, parentInComposite, pickAccordionAutoBinds, readLayerStyle, readProp, readStyle, removeFromComposite, renameAccordionItem, rowItemKey, runCompositeItemAction, serializeDomAttributes, setAccordionBehaviour, setAccordionIcon, setBadgeTone, setCardLinked, setCardPart, shouldScrollToItem, structureRefusal, walkComposite, writeLayerStyles, writeProp, writeStyles };
|
|
1602
1908
|
//# sourceMappingURL=vectoriox-iox-ui-composite-core.mjs.map
|