@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
package/index.d.ts
CHANGED
|
@@ -1530,7 +1530,9 @@ declare enum ComponentState {
|
|
|
1530
1530
|
}
|
|
1531
1531
|
/** The attribute selector each (non-targeted) component state compiles to. */
|
|
1532
1532
|
declare const COMPONENT_STATE_SELECTOR: Readonly<Record<string, string>>;
|
|
1533
|
-
/** The composite roles that are themselves "on" when their item is open (they carry `data-iox-on`)
|
|
1533
|
+
/** The composite roles that are themselves "on" when their item is open (they carry `data-iox-on`): an accordion's
|
|
1534
|
+
* item, header and panel; a disclosure (its own item), its toggle and its panel (CC2.1). The main entry does not
|
|
1535
|
+
* import composite-core, so the roles are spelled here — `style-compiler.spec` pins them to its enums. */
|
|
1534
1536
|
declare const ON_STATE_ROLES: ReadonlyArray<string>;
|
|
1535
1537
|
/**
|
|
1536
1538
|
* The component states a node offers in the Style panel's "Component state" group: Current page (a link),
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@ export type ActionKind = 'animated' | 'instant' | 'server';
|
|
|
11
11
|
|
|
12
12
|
/** Picker grouping. Flat lists die at ~10 entries; the group is data, never a second list. */
|
|
13
13
|
export type ActionGroup =
|
|
14
|
-
| 'motion' | 'visibility' | 'navigate' | 'dialog' | 'form' | 'data' | 'consent' | 'service';
|
|
14
|
+
| 'motion' | 'visibility' | 'navigate' | 'dialog' | 'item' | 'form' | 'data' | 'consent' | 'service';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* What a step does inside the live builder canvas (the canvas RUNS interactions):
|
|
@@ -29,10 +29,12 @@ export interface ActionParamSpec {
|
|
|
29
29
|
options?: readonly { value: string; labelKey: string }[];
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
/** Does this action point at another node — and may it be any node or only certain types? */
|
|
32
|
+
/** Does this action point at another node — and may it be any node, or only certain types / composite roles? */
|
|
33
33
|
export interface ActionTargetSpec {
|
|
34
34
|
required: boolean;
|
|
35
35
|
types?: string[];
|
|
36
|
+
/** Only nodes playing one of these composite roles (`openItem` targets an accordion item or a disclosure). */
|
|
37
|
+
roles?: string[];
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
/**
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { AccordionRole, CompositeItemAction, DisclosureRole } from '@vectoriox/iox-ui/composite-core';
|
|
2
|
+
|
|
1
3
|
import { ActionDescriptor } from './action.model';
|
|
2
4
|
|
|
5
|
+
/** The items an item action may target: an accordion's item, or a disclosure (its own one item). */
|
|
6
|
+
const ITEM_TARGET_ROLES = [AccordionRole.Item, DisclosureRole.Disclosure];
|
|
7
|
+
|
|
3
8
|
/**
|
|
4
9
|
* The ELEMENT action catalog (actions plan A1) — what a node can DO, as data. One list feeds the
|
|
5
10
|
* grouped picker, the two interaction engines, and the AI composer's validation (§9: the model
|
|
@@ -47,6 +52,16 @@ export const ELEMENT_ACTION_DESCRIPTORS: readonly ActionDescriptor[] = Object.fr
|
|
|
47
52
|
{ type: 'toggleDialog', labelKey: 'builder.actions.toggleDialog', kind: 'instant', group: 'dialog', builderBehaviour: 'run',
|
|
48
53
|
target: { required: true, types: ['Dialog'] } },
|
|
49
54
|
|
|
55
|
+
// ── Item (CC2.1 — composite T14): drive an accordion item or a disclosure from anywhere ("See shipping"
|
|
56
|
+
// opens FAQ item 3). The canvas RUNS them, like the dialog actions: it is the same runtime, and an item you
|
|
57
|
+
// cannot open from the button is a button you cannot design.
|
|
58
|
+
{ type: 'openItem', labelKey: 'builder.actions.openItem', kind: 'instant', group: 'item', builderBehaviour: 'run',
|
|
59
|
+
target: { required: true, roles: ITEM_TARGET_ROLES } },
|
|
60
|
+
{ type: 'closeItem', labelKey: 'builder.actions.closeItem', kind: 'instant', group: 'item', builderBehaviour: 'run',
|
|
61
|
+
target: { required: true, roles: ITEM_TARGET_ROLES } },
|
|
62
|
+
{ type: 'toggleItem', labelKey: 'builder.actions.toggleItem', kind: 'instant', group: 'item', builderBehaviour: 'run',
|
|
63
|
+
target: { required: true, roles: ITEM_TARGET_ROLES } },
|
|
64
|
+
|
|
50
65
|
// ── Form (A1 — Tier A; the submit transport is the forms module's, I7 of its plan) ──
|
|
51
66
|
// simulate in the CANVAS: the builder's Form is a container twin with no real <form>,
|
|
52
67
|
// so there is nothing honest to run — the live engine requestSubmit()s the real one.
|
|
@@ -112,6 +127,19 @@ export const SERVER_ELEMENT_ACTION_TYPES: ReadonlySet<string> =
|
|
|
112
127
|
export const BEHAVIOUR_ACTION_TYPES: ReadonlySet<string> =
|
|
113
128
|
new Set(['navigate', 'scrollTo', 'submitForm', 'copyToClipboard']);
|
|
114
129
|
|
|
130
|
+
/** The item actions' dispatch set (T14) and what each does to its item — the engines branch on it beside
|
|
131
|
+
* DIALOG/BEHAVIOUR, and composite-core `runCompositeItemAction` runs it on the target item's own runtime. */
|
|
132
|
+
export const ITEM_ACTION_TYPES: Readonly<Record<string, CompositeItemAction>> = Object.freeze({
|
|
133
|
+
openItem: CompositeItemAction.Open,
|
|
134
|
+
closeItem: CompositeItemAction.Close,
|
|
135
|
+
toggleItem: CompositeItemAction.Toggle,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
/** The item action an action type runs, or null when it is not one. */
|
|
139
|
+
export function itemActionOf(type: string | null | undefined): CompositeItemAction | null {
|
|
140
|
+
return (type && ITEM_ACTION_TYPES[type]) || null;
|
|
141
|
+
}
|
|
142
|
+
|
|
115
143
|
/** The consent actions' dispatch set (C3) — the engines branch on it beside DIALOG/BEHAVIOUR.
|
|
116
144
|
* `openConsentPreferences` is dialog-opening by nature and included so ONE branch owns consent. */
|
|
117
145
|
export const CONSENT_ACTION_TYPES: ReadonlySet<string> =
|
|
@@ -119,7 +147,7 @@ export const CONSENT_ACTION_TYPES: ReadonlySet<string> =
|
|
|
119
147
|
|
|
120
148
|
/** Picker order — most-used groups first, services last (they are the shop window, not the shop). */
|
|
121
149
|
export const ELEMENT_ACTION_GROUP_ORDER: readonly string[] =
|
|
122
|
-
Object.freeze(['motion', 'visibility', 'navigate', 'dialog', 'form', 'data', 'consent', 'service']);
|
|
150
|
+
Object.freeze(['motion', 'visibility', 'navigate', 'dialog', 'item', 'form', 'data', 'consent', 'service']);
|
|
123
151
|
|
|
124
152
|
/** The catalog, grouped and ordered for the picker — one derivation, no hand-kept second list. */
|
|
125
153
|
export function groupedElementActions(): ReadonlyArray<{ group: string; actions: ActionDescriptor[] }> {
|
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
import {
|
|
2
|
-
AccordionBehaviour, AccordionClass, AccordionDefaultOn, accordionDomIds, accordionFocusIndex, accordionKeyAction,
|
|
3
|
-
ACCORDION_DEFAULT_ONS, COMPOSITE_ATTRIBUTE, DEFAULT_ON_ATTRIBUTE, initialAccordionOpen, ITEM_KEY_ATTRIBUTE,
|
|
4
|
-
nextAccordionState, ON_ATTRIBUTE, openAccordionItem, UNTIL_FOUND,
|
|
2
|
+
AccordionBehaviour, AccordionClass, AccordionDefaultOn, accordionDomIds, accordionFocusIndex, accordionKeyAction,
|
|
3
|
+
ACCORDION_DEFAULT_ONS, closeAccordionItem, COMPOSITE_ATTRIBUTE, DEFAULT_ON_ATTRIBUTE, hasLabelledRegion, initialAccordionOpen, ITEM_KEY_ATTRIBUTE,
|
|
4
|
+
MULTIPLE_ATTRIBUTE, nextAccordionState, ON_ATTRIBUTE, openAccordionItem, SCOPE_ATTRIBUTE, UNTIL_FOUND,
|
|
5
5
|
} from './accordion';
|
|
6
|
+
import { CompositeKind } from './composite-kinds';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* The Accordion's RUNTIME (CC2) — ONE copy, DOM only, for the builder canvas and the live site (both engine
|
|
9
10
|
* paths render the same markup). It reads the markup the renderers write (iox-design.css + accordion.ts's
|
|
10
11
|
* attribute planners), so the server's first paint and every later state agree:
|
|
11
12
|
*
|
|
12
|
-
* [root] data-iox-composite="accordion" data-iox-default-on [data-iox-multiple]
|
|
13
|
-
* its role class may sit on a wrapper host, as the engine's component-path Repeater draws it)
|
|
13
|
+
* [root] data-iox-composite="accordion" data-iox-default-on [data-iox-multiple] [data-iox-scope] (the node's
|
|
14
|
+
* own element — its role class may sit on a wrapper host, as the engine's component-path Repeater draws it)
|
|
14
15
|
* .iox-role-accordion-item data-iox-item-key [data-iox-on]
|
|
15
16
|
* hN.iox-accordion-heading > button.iox-accordion-button id aria-expanded aria-controls
|
|
16
17
|
* .iox-accordion-track [data-iox-on] > .iox-accordion-region role=region [hidden=until-found]
|
|
17
18
|
* .iox-role-accordion-panel
|
|
18
19
|
*
|
|
20
|
+
* The DISCLOSURE (CC2.1) runs on this same runtime: `data-iox-composite="disclosure"` on a root that IS its one
|
|
21
|
+
* item (it carries the key and `data-iox-on`), its button straight inside it (no heading), its panel in the same
|
|
22
|
+
* track + region (a plain block — no `role=region`), and no arrow keys (one button).
|
|
23
|
+
*
|
|
19
24
|
* It owns: a click on a header toggles its item; ↓ ↑ Home End move between the headers of THIS accordion
|
|
20
25
|
* (a nested one keeps its own); find-in-page (`beforematch`) opens the item that holds the match; a closed
|
|
21
26
|
* panel gets `hidden="until-found"` after its track has collapsed (at once under reduced motion). Every
|
|
@@ -27,9 +32,12 @@ import {
|
|
|
27
32
|
export const ITEM_ON_EVENT = 'iox-item-on';
|
|
28
33
|
export const ITEM_OFF_EVENT = 'iox-item-off';
|
|
29
34
|
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
35
|
+
const KIND_ROOT = (kind: CompositeKind) => `[${COMPOSITE_ATTRIBUTE}="${kind}"]`;
|
|
36
|
+
const ROOT_SELECTOR = Object.values(CompositeKind).map(KIND_ROOT).join(', ');
|
|
37
|
+
/** An accordion's items are its `.iox-role-accordion-item` children; a disclosure is its own item. */
|
|
38
|
+
const ACCORDION_ITEM_SELECTOR = '.iox-role-accordion-item';
|
|
39
|
+
const ITEM_SELECTOR = `${ACCORDION_ITEM_SELECTOR}, ${KIND_ROOT(CompositeKind.Disclosure)}`;
|
|
40
|
+
const PANEL_SELECTOR = '.iox-role-accordion-panel, .iox-role-disclosure-panel';
|
|
33
41
|
const BUTTON_SELECTOR = `.${AccordionClass.Button}`;
|
|
34
42
|
const TRACK_SELECTOR = `.${AccordionClass.Track}`;
|
|
35
43
|
const REGION_SELECTOR = `.${AccordionClass.Region}`;
|
|
@@ -38,8 +46,11 @@ const MAX_CLOSE_MS = 600;
|
|
|
38
46
|
const REDUCED_MOTION = '(prefers-reduced-motion: reduce)';
|
|
39
47
|
|
|
40
48
|
export interface AccordionRuntimeOptions {
|
|
41
|
-
/** The
|
|
42
|
-
|
|
49
|
+
/** The composite's scope id — the ARIA ids' prefix (else the root's `data-iox-scope`, else its `iox-node-<id>`). A
|
|
50
|
+
* function is read at every stamp (the builder canvas: a preview row's index is known once it is in the page). */
|
|
51
|
+
rootId?: string | (() => string);
|
|
52
|
+
/** Accordion or Disclosure (else read from the root's `data-iox-composite`). */
|
|
53
|
+
kind?: CompositeKind;
|
|
43
54
|
/** The behaviour (else read from the root's data attributes). */
|
|
44
55
|
behaviour?: AccordionBehaviour;
|
|
45
56
|
/** Keys forced open on top of the live state (the builder's design state). */
|
|
@@ -50,16 +61,24 @@ export interface AccordionRuntimeOptions {
|
|
|
50
61
|
reducedMotion?: () => boolean;
|
|
51
62
|
}
|
|
52
63
|
|
|
53
|
-
/** The accordion an element belongs to (the element itself when it is a root). */
|
|
64
|
+
/** The composite (accordion or disclosure) an element belongs to (the element itself when it is a root). */
|
|
54
65
|
export function closestAccordion(el: Element | null | undefined): HTMLElement | null {
|
|
55
66
|
return (el?.closest?.(ROOT_SELECTOR) as HTMLElement | null) ?? null;
|
|
56
67
|
}
|
|
57
68
|
|
|
58
|
-
/** The item an element sits in (itself when it is one). */
|
|
69
|
+
/** The item an element sits in (itself when it is one) — an accordion item, or a disclosure. */
|
|
59
70
|
export function closestAccordionItem(el: Element | null | undefined): HTMLElement | null {
|
|
60
71
|
return (el?.closest?.(ITEM_SELECTOR) as HTMLElement | null) ?? null;
|
|
61
72
|
}
|
|
62
73
|
|
|
74
|
+
const kindOf = (root: Element): CompositeKind =>
|
|
75
|
+
root.getAttribute(COMPOSITE_ATTRIBUTE) === CompositeKind.Disclosure ? CompositeKind.Disclosure : CompositeKind.Accordion;
|
|
76
|
+
|
|
77
|
+
/** The composite root whose items an item belongs to: a disclosure is its own root. */
|
|
78
|
+
function rootOfItem(item: HTMLElement): HTMLElement | null {
|
|
79
|
+
return kindOf(item) === CompositeKind.Disclosure && item.matches(KIND_ROOT(CompositeKind.Disclosure)) ? item : closestAccordion(item.parentElement);
|
|
80
|
+
}
|
|
81
|
+
|
|
63
82
|
/** Subscribe to an item turning on / off — what the `itemOn` / `itemOff` interaction triggers listen to. The
|
|
64
83
|
* item is the element's own item (the element itself when it is one). Returns the unsubscribe. */
|
|
65
84
|
export function onAccordionItemState(el: Element, on: boolean, handler: () => void): () => void {
|
|
@@ -103,10 +122,13 @@ export class AccordionRuntime {
|
|
|
103
122
|
private behaviourValue: AccordionBehaviour;
|
|
104
123
|
/** The builder attaches a root before its items render: the saved default is applied once they do. */
|
|
105
124
|
private awaitingItems = false;
|
|
125
|
+
readonly kind: CompositeKind;
|
|
106
126
|
|
|
107
127
|
constructor(readonly root: HTMLElement, private readonly options: AccordionRuntimeOptions = {}) {
|
|
128
|
+
this.kind = options.kind ?? kindOf(root);
|
|
108
129
|
this.behaviourValue = options.behaviour ?? behaviourOf(root);
|
|
109
|
-
root.setAttribute(COMPOSITE_ATTRIBUTE,
|
|
130
|
+
root.setAttribute(COMPOSITE_ATTRIBUTE, this.kind);
|
|
131
|
+
RUNTIMES.set(root, this);
|
|
110
132
|
// The server rendered the first state; with no server state the root's default decides.
|
|
111
133
|
// (The builder passes the behaviour and starts from the saved default; on the live site the server
|
|
112
134
|
// stamped every item's key and state, and that is the truth.)
|
|
@@ -144,9 +166,10 @@ export class AccordionRuntime {
|
|
|
144
166
|
else this.stamp();
|
|
145
167
|
}
|
|
146
168
|
|
|
147
|
-
/** THIS
|
|
169
|
+
/** THIS composite's items, in document order (a nested composite's items are its own); a disclosure's is itself. */
|
|
148
170
|
items(): HTMLElement[] {
|
|
149
|
-
|
|
171
|
+
if (this.kind === CompositeKind.Disclosure) return [this.root];
|
|
172
|
+
return Array.from(this.root.querySelectorAll<HTMLElement>(ACCORDION_ITEM_SELECTOR)).filter(i => closestAccordion(i.parentElement) === this.root);
|
|
150
173
|
}
|
|
151
174
|
|
|
152
175
|
/** The open keys the live state holds (before anything forced). */
|
|
@@ -173,6 +196,19 @@ export class AccordionRuntime {
|
|
|
173
196
|
this.stamp();
|
|
174
197
|
}
|
|
175
198
|
|
|
199
|
+
close(key: string): void {
|
|
200
|
+
this.live = closeAccordionItem(this.live, key);
|
|
201
|
+
this.stamp();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Run an item action (`openItem` / `closeItem` / `toggleItem`, T14) on `key`. True when it shows open after. */
|
|
205
|
+
run(action: CompositeItemAction, key: string): boolean {
|
|
206
|
+
if (action === CompositeItemAction.Open) this.open(key);
|
|
207
|
+
else if (action === CompositeItemAction.Close) this.close(key);
|
|
208
|
+
else this.toggle(key);
|
|
209
|
+
return this.shown.has(key);
|
|
210
|
+
}
|
|
211
|
+
|
|
176
212
|
/** Back to the saved default ("Showing on canvas ▸ As saved"). */
|
|
177
213
|
resetLive(): void {
|
|
178
214
|
const items = this.items();
|
|
@@ -186,6 +222,7 @@ export class AccordionRuntime {
|
|
|
186
222
|
}
|
|
187
223
|
|
|
188
224
|
destroy(): void {
|
|
225
|
+
if (RUNTIMES.get(this.root) === this) RUNTIMES.delete(this.root);
|
|
189
226
|
this.root.removeEventListener('click', this.onClick, true);
|
|
190
227
|
this.root.removeEventListener('keydown', this.onKeydown);
|
|
191
228
|
this.root.removeEventListener('beforematch', this.onBeforeMatch, true);
|
|
@@ -217,7 +254,8 @@ export class AccordionRuntime {
|
|
|
217
254
|
first = true;
|
|
218
255
|
}
|
|
219
256
|
const forced = this.options.forced?.() ?? new Set<string>();
|
|
220
|
-
const
|
|
257
|
+
const own = this.options.rootId;
|
|
258
|
+
const rootId = (typeof own === 'function' ? own() : own) ?? this.root.getAttribute(SCOPE_ATTRIBUTE) ?? nodeIdOf(this.root);
|
|
221
259
|
const next = new Set<string>();
|
|
222
260
|
items.forEach((item, index) => {
|
|
223
261
|
const key = this.keyOf(item, items);
|
|
@@ -238,7 +276,7 @@ export class AccordionRuntime {
|
|
|
238
276
|
}
|
|
239
277
|
|
|
240
278
|
private stampItem(item: HTMLElement, key: string, index: number, on: boolean, rootId: string, first: boolean): void {
|
|
241
|
-
const ids = accordionDomIds(rootId, key);
|
|
279
|
+
const ids = accordionDomIds(rootId, key, this.kind);
|
|
242
280
|
if (!item.hasAttribute(ITEM_KEY_ATTRIBUTE)) item.setAttribute(ITEM_KEY_ATTRIBUTE, key);
|
|
243
281
|
setAttr(item, ON_ATTRIBUTE, on ? '' : null);
|
|
244
282
|
const button = this.own(item, BUTTON_SELECTOR);
|
|
@@ -252,9 +290,10 @@ export class AccordionRuntime {
|
|
|
252
290
|
setAttr(button, ON_ATTRIBUTE, on ? '' : null);
|
|
253
291
|
setAttr(track, ON_ATTRIBUTE, on ? '' : null);
|
|
254
292
|
setAttr(panel, ON_ATTRIBUTE, on ? '' : null);
|
|
255
|
-
|
|
293
|
+
const labelled = hasLabelledRegion(this.kind);
|
|
294
|
+
setAttr(region, 'role', labelled ? 'region' : null);
|
|
256
295
|
setAttr(region, 'id', ids.panel);
|
|
257
|
-
setAttr(region, 'aria-labelledby', ids.trigger);
|
|
296
|
+
setAttr(region, 'aria-labelledby', labelled ? ids.trigger : null);
|
|
258
297
|
if (!region) return;
|
|
259
298
|
const pending = this.closing.get(key);
|
|
260
299
|
if (on) {
|
|
@@ -301,6 +340,8 @@ export class AccordionRuntime {
|
|
|
301
340
|
};
|
|
302
341
|
|
|
303
342
|
private readonly onKeydown = (event: Event): void => {
|
|
343
|
+
// A disclosure has one button: Enter / Space are the button's own, and there is nowhere to move.
|
|
344
|
+
if (this.kind !== CompositeKind.Accordion) return;
|
|
304
345
|
const e = event as KeyboardEvent;
|
|
305
346
|
const action = accordionKeyAction(e.key);
|
|
306
347
|
const button = (e.target as Element | null)?.closest?.(BUTTON_SELECTOR);
|
|
@@ -316,8 +357,9 @@ export class AccordionRuntime {
|
|
|
316
357
|
private readonly onBeforeMatch = (event: Event): void => {
|
|
317
358
|
const region = (event.target as Element | null)?.closest?.(REGION_SELECTOR);
|
|
318
359
|
const item = closestAccordionItem(region);
|
|
319
|
-
|
|
320
|
-
|
|
360
|
+
const items = this.items();
|
|
361
|
+
if (!item || !items.includes(item)) return;
|
|
362
|
+
this.open(this.keyOf(item, items));
|
|
321
363
|
};
|
|
322
364
|
}
|
|
323
365
|
|
|
@@ -326,13 +368,11 @@ function dispatch(el: Element, type: string): void {
|
|
|
326
368
|
if (Ctor) el.dispatchEvent(new Ctor(type, { bubbles: false }));
|
|
327
369
|
}
|
|
328
370
|
|
|
329
|
-
/** Attach the runtime to one rendered
|
|
371
|
+
/** Attach the runtime to one rendered composite root, unless it already runs one. Returns the NEW runtime, or
|
|
330
372
|
* null when the root already had one (so a caller destroys only what it created). */
|
|
331
373
|
export function attachAccordionAt(root: HTMLElement, options: Omit<AccordionRuntimeOptions, 'rootId' | 'behaviour'> = {}): AccordionRuntime | null {
|
|
332
|
-
if (
|
|
333
|
-
|
|
334
|
-
ATTACHED.set(root, runtime);
|
|
335
|
-
return runtime;
|
|
374
|
+
if (RUNTIMES.has(root)) return null;
|
|
375
|
+
return new AccordionRuntime(root, options);
|
|
336
376
|
}
|
|
337
377
|
|
|
338
378
|
/** Attach the runtime to every accordion under `container` that has none yet. Returns the new runtimes. */
|
|
@@ -348,7 +388,56 @@ export function attachAccordions(container: ParentNode, options: Omit<AccordionR
|
|
|
348
388
|
/** Detach a runtime `attachAccordions` created. */
|
|
349
389
|
export function detachAccordion(runtime: AccordionRuntime): void {
|
|
350
390
|
runtime.destroy();
|
|
351
|
-
ATTACHED.delete(runtime.root);
|
|
352
391
|
}
|
|
353
392
|
|
|
354
|
-
|
|
393
|
+
/** Every live runtime by its root — the canvas's and the site's alike (a runtime registers itself and leaves on
|
|
394
|
+
* destroy), so an item action finds the runtime of whatever item it targets. */
|
|
395
|
+
const RUNTIMES = new WeakMap<Element, AccordionRuntime>();
|
|
396
|
+
|
|
397
|
+
const ROW_INDEX_ATTRIBUTE = 'data-iox-row-idx';
|
|
398
|
+
|
|
399
|
+
/** The repeated rows an element sits in, outermost first — each row's index as its row wrappers stamp it
|
|
400
|
+
* (`data-iox-row-idx`: the builder's Repeater rows, the engine's component path). The builder folds them into a
|
|
401
|
+
* composite's scope id (`compositeScopeId`, vectoriox/iox-ui#20); the element itself is not counted. */
|
|
402
|
+
export function compositeRowKeys(el: Element | null | undefined): string[] {
|
|
403
|
+
const keys: string[] = [];
|
|
404
|
+
for (let at = el?.parentElement?.closest?.(`[${ROW_INDEX_ATTRIBUTE}]`); at; at = at.parentElement?.closest(`[${ROW_INDEX_ATTRIBUTE}]`)) {
|
|
405
|
+
keys.unshift(at.getAttribute(ROW_INDEX_ATTRIBUTE) ?? '');
|
|
406
|
+
}
|
|
407
|
+
return keys;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** The runtime running a composite root, or null. */
|
|
411
|
+
export function accordionRuntimeOf(root: Element | null | undefined): AccordionRuntime | null {
|
|
412
|
+
return (root && RUNTIMES.get(root)) || null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ── T14: open / close / toggle an item from anywhere ─────────────────────────────────────────────
|
|
416
|
+
|
|
417
|
+
/** What an item action does to its target item (actions-core `openItem` / `closeItem` / `toggleItem`). */
|
|
418
|
+
export enum CompositeItemAction {
|
|
419
|
+
Open = 'open',
|
|
420
|
+
Close = 'close',
|
|
421
|
+
Toggle = 'toggle',
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Run an item action on the element of an ITEM (an accordion item, or a disclosure) — the `openItem` /
|
|
426
|
+
* `closeItem` / `toggleItem` interaction actions, on the canvas and on the live site through this one function.
|
|
427
|
+
* Returns whether the item shows open afterwards, or null when `el` is in no running composite.
|
|
428
|
+
*/
|
|
429
|
+
export function runCompositeItemAction(el: Element | null | undefined, action: CompositeItemAction): boolean | null {
|
|
430
|
+
const item = closestAccordionItem(el);
|
|
431
|
+
const runtime = item ? accordionRuntimeOf(rootOfItem(item)) : null;
|
|
432
|
+
if (!item || !runtime) return null;
|
|
433
|
+
return runtime.run(action, runtime.keyOf(item));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** How much of an opened item must already be on screen for the action NOT to scroll to it (its header row). */
|
|
437
|
+
const VISIBLE_HEAD_PX = 48;
|
|
438
|
+
|
|
439
|
+
/** True when an item an action just opened is out of view and the page should scroll to it (T14: "opens that item
|
|
440
|
+
* and scrolls to it") — never when its head is already on screen, so a button beside it does not jolt the page. */
|
|
441
|
+
export function shouldScrollToItem(top: number, viewportHeight: number): boolean {
|
|
442
|
+
return top < 0 || top + VISIBLE_HEAD_PX > viewportHeight;
|
|
443
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { readOwnedTrait } from '@vectoriox/iox-ui/design-core';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
AccordionRole, COMPOSITE_KIND_ROLES, CompositeKind, compositeKindOfRole,
|
|
5
|
+
} from './composite-kinds';
|
|
6
|
+
import {
|
|
7
|
+
CompositeNode, CompositeNodeFactory, findByRole, makeCompositePart, readProp, removeFromComposite, walkComposite, writeProp, writeStyles,
|
|
5
8
|
} from './composite-node';
|
|
6
9
|
|
|
7
10
|
/**
|
|
@@ -23,14 +26,6 @@ import {
|
|
|
23
26
|
* functions), the keyboard one key map — read by the builder canvas, both engine paths and the composer.
|
|
24
27
|
*/
|
|
25
28
|
|
|
26
|
-
export enum AccordionRole {
|
|
27
|
-
Accordion = 'accordion',
|
|
28
|
-
Item = 'accordion.item',
|
|
29
|
-
Trigger = 'accordion.trigger',
|
|
30
|
-
Icon = 'accordion.icon',
|
|
31
|
-
Panel = 'accordion.panel',
|
|
32
|
-
}
|
|
33
|
-
|
|
34
29
|
/** The behaviour options a composite root stores as ordinary TRAITS (composite plan §3). */
|
|
35
30
|
export enum CompositeBehaviourTrait {
|
|
36
31
|
/** Several items may be open at once ("Allow several open"). */
|
|
@@ -135,17 +130,15 @@ export interface AccordionOptions {
|
|
|
135
130
|
|
|
136
131
|
function make(create: CompositeNodeFactory, key: string, type: string, role: AccordionRole | null, style: string,
|
|
137
132
|
props: Record<string, unknown> = {}, label?: string): CompositeNode {
|
|
138
|
-
|
|
139
|
-
if (role) node.role = role;
|
|
140
|
-
if (label) node.label = label;
|
|
141
|
-
if (role && PART_BASE[role]) writeStyles(node, PART_BASE[role]);
|
|
142
|
-
return node;
|
|
133
|
+
return makeCompositePart(create, { key, type, props, elementStyle: style }, role, label, role ? PART_BASE[role] : undefined);
|
|
143
134
|
}
|
|
144
135
|
|
|
145
|
-
/** The item's
|
|
146
|
-
|
|
136
|
+
/** The icon of an item (an accordion's, or a disclosure's — `role`); its Item-open state (the turn) is driven by
|
|
137
|
+
* ITS item (`stateTriggers`). ONE builder for both kinds. */
|
|
138
|
+
export function buildCompositeIcon(create: CompositeNodeFactory, key: string, icon: Exclude<AccordionIcon, AccordionIcon.None>, itemId?: string,
|
|
139
|
+
role: string = AccordionRole.Icon): CompositeNode {
|
|
147
140
|
const glyph = ACCORDION_ICON_GLYPH[icon];
|
|
148
|
-
const node =
|
|
141
|
+
const node = makeCompositePart(create, { key, type: Type.Icon, props: { iconClass: glyph.iconClass }, elementStyle: Style.Icon }, role, 'Icon', PART_BASE[AccordionRole.Icon]);
|
|
149
142
|
node.stateStyles = { ...(node.stateStyles ?? {}), [ITEM_OPEN_STATE]: { rotate: glyph.turn } };
|
|
150
143
|
if (itemId) node.stateTriggers = { ...(node.stateTriggers ?? {}), [ITEM_OPEN_STATE]: itemId };
|
|
151
144
|
return node;
|
|
@@ -158,7 +151,7 @@ export function buildAccordionItem(create: CompositeNodeFactory, words: Accordio
|
|
|
158
151
|
const trigger = make(create, `${itemKey}-trigger`, Type.LinkedContainer, AccordionRole.Trigger, Style.Trigger, {}, 'Header');
|
|
159
152
|
const question = make(create, `${itemKey}-question`, Type.Heading, null, Style.Question, { content: words.question, level: DEFAULT_LEVEL }, 'Heading');
|
|
160
153
|
writeStyles(question, QUESTION_BASE);
|
|
161
|
-
trigger.children = [question, ...(icon === AccordionIcon.None ? [] : [
|
|
154
|
+
trigger.children = [question, ...(icon === AccordionIcon.None ? [] : [buildCompositeIcon(create, `${itemKey}-icon`, icon, item.id)])];
|
|
162
155
|
const panel = make(create, `${itemKey}-panel`, Type.Container, AccordionRole.Panel, Style.Panel, {}, 'Panel');
|
|
163
156
|
const answer = make(create, `${itemKey}-answer`, Type.Text, null, Style.Answer, { content: words.answer, tag: 'p' }, 'Text');
|
|
164
157
|
writeStyles(answer, ANSWER_BASE);
|
|
@@ -238,22 +231,37 @@ export function setAccordionBehaviour(root: CompositeNode, patch: Partial<Accord
|
|
|
238
231
|
return ok;
|
|
239
232
|
}
|
|
240
233
|
|
|
241
|
-
/** The
|
|
234
|
+
/** The roles of the kind an item belongs to (an accordion item, or a disclosure — its own item). */
|
|
235
|
+
function kindRolesOf(node: CompositeNode) {
|
|
236
|
+
return COMPOSITE_KIND_ROLES[compositeKindOfRole(node.role) ?? CompositeKind.Accordion];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** The header of an item (either kind). */
|
|
242
240
|
export function accordionTriggerOf(item: CompositeNode): CompositeNode | undefined {
|
|
243
|
-
return findByRole(item,
|
|
241
|
+
return findByRole(item, kindRolesOf(item).trigger);
|
|
244
242
|
}
|
|
245
243
|
|
|
246
|
-
/** The panel of an item. */
|
|
244
|
+
/** The panel of an item (either kind). */
|
|
247
245
|
export function accordionPanelOf(item: CompositeNode): CompositeNode | undefined {
|
|
248
|
-
return findByRole(item,
|
|
246
|
+
return findByRole(item, kindRolesOf(item).panel);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** A root's items — an accordion's (`accordionItems`), or a disclosure, which is its own one item. */
|
|
250
|
+
export function compositeItems(root: CompositeNode): CompositeNode[] {
|
|
251
|
+
return compositeKindOfRole(root.role) === CompositeKind.Disclosure ? [root] : accordionItems(root);
|
|
249
252
|
}
|
|
250
253
|
|
|
251
|
-
/** The question: the first Heading inside the header
|
|
254
|
+
/** The question: the first Heading inside the header — a disclosure's toggle holds no heading, so its first word
|
|
255
|
+
* (the Text shown while closed, "Read more"). */
|
|
252
256
|
export function accordionQuestionOf(item: CompositeNode): CompositeNode | undefined {
|
|
253
257
|
const trigger = accordionTriggerOf(item);
|
|
254
258
|
let hit: CompositeNode | undefined;
|
|
255
|
-
|
|
256
|
-
|
|
259
|
+
let text: CompositeNode | undefined;
|
|
260
|
+
if (trigger) walkComposite(trigger, n => {
|
|
261
|
+
if (!hit && n.type === Type.Heading) hit = n;
|
|
262
|
+
if (!text && n.type === Type.Text) text = n;
|
|
263
|
+
});
|
|
264
|
+
return hit ?? text;
|
|
257
265
|
}
|
|
258
266
|
|
|
259
267
|
/** The answer: the first Text inside the panel. */
|
|
@@ -337,10 +345,10 @@ export function accordionRemovalRefusal(root: CompositeNode | null | undefined,
|
|
|
337
345
|
return accordionItems(root).length <= 1 ? AccordionEditRefusal.LastItem : null;
|
|
338
346
|
}
|
|
339
347
|
|
|
340
|
-
/** The icon the
|
|
348
|
+
/** The icon the composite's items wear now (the first item's; None when it has no icon). Either kind. */
|
|
341
349
|
export function detectAccordionIcon(root: CompositeNode): AccordionIcon {
|
|
342
|
-
const first =
|
|
343
|
-
const icon = first ? findByRole(first,
|
|
350
|
+
const first = compositeItems(root)[0];
|
|
351
|
+
const icon = first ? findByRole(first, kindRolesOf(root).icon) : undefined;
|
|
344
352
|
if (!icon) return first ? AccordionIcon.None : AccordionIcon.Chevron;
|
|
345
353
|
const cls = String(readProp(icon, 'iconClass') ?? '');
|
|
346
354
|
return cls.includes(ACCORDION_ICON_GLYPH[AccordionIcon.Plus].iconClass.split(' ')[1]) ? AccordionIcon.Plus : AccordionIcon.Chevron;
|
|
@@ -353,10 +361,11 @@ export function detectAccordionIcon(root: CompositeNode): AccordionIcon {
|
|
|
353
361
|
*/
|
|
354
362
|
export function setAccordionIcon(root: CompositeNode, icon: AccordionIcon, create?: CompositeNodeFactory): number {
|
|
355
363
|
let changed = 0;
|
|
356
|
-
|
|
364
|
+
const iconRole = kindRolesOf(root).icon;
|
|
365
|
+
for (const item of compositeItems(root)) {
|
|
357
366
|
const trigger = accordionTriggerOf(item);
|
|
358
367
|
if (!trigger) continue;
|
|
359
|
-
const existing = findByRole(trigger,
|
|
368
|
+
const existing = findByRole(trigger, iconRole);
|
|
360
369
|
if (icon === AccordionIcon.None) {
|
|
361
370
|
if (existing && removeFromComposite(trigger, existing)) changed++;
|
|
362
371
|
continue;
|
|
@@ -367,7 +376,7 @@ export function setAccordionIcon(root: CompositeNode, icon: AccordionIcon, creat
|
|
|
367
376
|
existing.stateStyles = { ...(existing.stateStyles ?? {}), [ITEM_OPEN_STATE]: { ...(existing.stateStyles?.[ITEM_OPEN_STATE] ?? {}), rotate: glyph.turn } };
|
|
368
377
|
changed++;
|
|
369
378
|
} else if (create) {
|
|
370
|
-
trigger.children = [...(trigger.children ?? []),
|
|
379
|
+
trigger.children = [...(trigger.children ?? []), buildCompositeIcon(create, `${item.itemKey ?? 'item'}-icon`, icon, item.id, iconRole)];
|
|
371
380
|
changed++;
|
|
372
381
|
}
|
|
373
382
|
}
|
|
@@ -404,11 +413,18 @@ export function nextAccordionState(open: ReadonlySet<string>, key: string, behav
|
|
|
404
413
|
return behaviour.multiple ? new Set([...open, key]) : new Set([key]);
|
|
405
414
|
}
|
|
406
415
|
|
|
407
|
-
/** The open keys after OPENING `key` (find-in-page, selecting into a closed panel). */
|
|
416
|
+
/** The open keys after OPENING `key` (find-in-page, selecting into a closed panel, the `openItem` action). */
|
|
408
417
|
export function openAccordionItem(open: ReadonlySet<string>, key: string, behaviour: Pick<AccordionBehaviour, 'multiple'>): Set<string> {
|
|
409
418
|
return open.has(key) ? new Set(open) : nextAccordionState(open, key, behaviour);
|
|
410
419
|
}
|
|
411
420
|
|
|
421
|
+
/** The open keys after CLOSING `key` (the `closeItem` action). */
|
|
422
|
+
export function closeAccordionItem(open: ReadonlySet<string>, key: string): Set<string> {
|
|
423
|
+
const next = new Set(open);
|
|
424
|
+
next.delete(key);
|
|
425
|
+
return next;
|
|
426
|
+
}
|
|
427
|
+
|
|
412
428
|
export enum AccordionKeyAction {
|
|
413
429
|
Next = 'next',
|
|
414
430
|
Previous = 'previous',
|
|
@@ -460,35 +476,52 @@ export interface AccordionDomIds {
|
|
|
460
476
|
panel: string;
|
|
461
477
|
}
|
|
462
478
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
479
|
+
const DOM_ID_PREFIX: Readonly<Record<CompositeKind, string>> = {
|
|
480
|
+
[CompositeKind.Accordion]: 'iox-acc',
|
|
481
|
+
[CompositeKind.Disclosure]: 'iox-dis',
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
/** The ARIA pairing ids of an item: unique per composite (its SCOPE id — `compositeScopeId`) and per item (its key). */
|
|
485
|
+
export function accordionDomIds(rootId: string, itemKey: string, kind: CompositeKind = CompositeKind.Accordion): AccordionDomIds {
|
|
486
|
+
const base = `${DOM_ID_PREFIX[kind]}-${domSafeKey(rootId)}-${domSafeKey(itemKey)}`;
|
|
466
487
|
return { trigger: `${base}-h`, panel: `${base}-p` };
|
|
467
488
|
}
|
|
468
489
|
|
|
490
|
+
/**
|
|
491
|
+
* The id a rendered composite's ARIA ids are built from: its node id, and — when it sits inside repeated rows —
|
|
492
|
+
* every enclosing row's identity, outermost first (vectoriox/iox-ui#20: an accordion repeated per card used to
|
|
493
|
+
* pair every row's header with the FIRST row's panel). The engine folds each row's `rowItemKey`; the builder
|
|
494
|
+
* canvas its preview row's index. Unrepeated, it is the node id unchanged.
|
|
495
|
+
*/
|
|
496
|
+
export function compositeScopeId(rootId: string, rowKeys: ReadonlyArray<string> = []): string {
|
|
497
|
+
return rowKeys.length ? [rootId, ...rowKeys.map(domSafeKey)].join('-') : rootId;
|
|
498
|
+
}
|
|
499
|
+
|
|
469
500
|
/** One item's render state: its identity, whether it is open, and its ARIA ids. */
|
|
470
501
|
export interface AccordionItemState {
|
|
502
|
+
kind: CompositeKind;
|
|
471
503
|
key: string;
|
|
472
504
|
index: number;
|
|
473
505
|
on: boolean;
|
|
474
506
|
ids: AccordionDomIds;
|
|
475
507
|
}
|
|
476
508
|
|
|
477
|
-
/** The render scope of one
|
|
509
|
+
/** The render scope of one composite: its kind, scope id, behaviour and every item key in order. */
|
|
478
510
|
export interface AccordionScope {
|
|
511
|
+
kind: CompositeKind;
|
|
479
512
|
rootId: string;
|
|
480
513
|
behaviour: AccordionBehaviour;
|
|
481
514
|
keys: ReadonlyArray<string>;
|
|
482
515
|
open: ReadonlySet<string>;
|
|
483
516
|
}
|
|
484
517
|
|
|
485
|
-
export function accordionScope(rootId: string, behaviour: AccordionBehaviour, keys: ReadonlyArray<string
|
|
486
|
-
return { rootId, behaviour, keys, open: initialAccordionOpen(keys, behaviour.defaultOn) };
|
|
518
|
+
export function accordionScope(rootId: string, behaviour: AccordionBehaviour, keys: ReadonlyArray<string>, kind: CompositeKind = CompositeKind.Accordion): AccordionScope {
|
|
519
|
+
return { kind, rootId, behaviour, keys, open: initialAccordionOpen(keys, behaviour.defaultOn) };
|
|
487
520
|
}
|
|
488
521
|
|
|
489
522
|
export function accordionItemState(scope: AccordionScope, index: number): AccordionItemState {
|
|
490
523
|
const key = scope.keys[index] ?? `item-${index}`;
|
|
491
|
-
return { key, index, on: scope.open.has(key), ids: accordionDomIds(scope.rootId, key) };
|
|
524
|
+
return { kind: scope.kind, key, index, on: scope.open.has(key), ids: accordionDomIds(scope.rootId, key, scope.kind) };
|
|
492
525
|
}
|
|
493
526
|
|
|
494
527
|
/** Attribute values: a string, or '' for a present boolean attribute. Absent attributes are left out. */
|
|
@@ -500,6 +533,9 @@ export const ITEM_KEY_ATTRIBUTE = 'data-iox-item-key';
|
|
|
500
533
|
export const COMPOSITE_ATTRIBUTE = 'data-iox-composite';
|
|
501
534
|
export const MULTIPLE_ATTRIBUTE = 'data-iox-multiple';
|
|
502
535
|
export const DEFAULT_ON_ATTRIBUTE = 'data-iox-default-on';
|
|
536
|
+
/** The scope id the ARIA ids are built from (`compositeScopeId`), written by the engine so the runtime re-stamps
|
|
537
|
+
* exactly the ids the server rendered. */
|
|
538
|
+
export const SCOPE_ATTRIBUTE = 'data-iox-scope';
|
|
503
539
|
/** The value a closed panel's region carries: find-in-page can still reveal it (D9, board 4). */
|
|
504
540
|
export const UNTIL_FOUND = 'until-found';
|
|
505
541
|
|
|
@@ -511,11 +547,12 @@ export enum AccordionClass {
|
|
|
511
547
|
Region = 'iox-accordion-region',
|
|
512
548
|
}
|
|
513
549
|
|
|
514
|
-
export function accordionRootAttributes(behaviour: AccordionBehaviour): DomAttributes {
|
|
550
|
+
export function accordionRootAttributes(behaviour: AccordionBehaviour, kind: CompositeKind = CompositeKind.Accordion, scopeId?: string): DomAttributes {
|
|
515
551
|
return {
|
|
516
|
-
[COMPOSITE_ATTRIBUTE]:
|
|
552
|
+
[COMPOSITE_ATTRIBUTE]: kind,
|
|
517
553
|
[DEFAULT_ON_ATTRIBUTE]: behaviour.defaultOn,
|
|
518
|
-
...(behaviour.multiple ? { [MULTIPLE_ATTRIBUTE]: '' } : {}),
|
|
554
|
+
...(behaviour.multiple && kind === CompositeKind.Accordion ? { [MULTIPLE_ATTRIBUTE]: '' } : {}),
|
|
555
|
+
...(scopeId ? { [SCOPE_ATTRIBUTE]: scopeId } : {}),
|
|
519
556
|
};
|
|
520
557
|
}
|
|
521
558
|
|
|
@@ -538,16 +575,23 @@ export function accordionTrackAttributes(state: AccordionItemState): DomAttribut
|
|
|
538
575
|
return state.on ? { [ON_ATTRIBUTE]: '' } : {};
|
|
539
576
|
}
|
|
540
577
|
|
|
541
|
-
/** The region the panel sits in: `hidden="until-found"` while closed, so its words stay in the page.
|
|
578
|
+
/** The region the panel sits in: `hidden="until-found"` while closed, so its words stay in the page. An accordion's
|
|
579
|
+
* is a labelled `region` (APG accordion); a disclosure's is a plain block (APG disclosure: the button names it). */
|
|
542
580
|
export function accordionRegionAttributes(state: AccordionItemState): DomAttributes {
|
|
581
|
+
const labelled = hasLabelledRegion(state.kind);
|
|
543
582
|
return {
|
|
544
|
-
role: 'region',
|
|
583
|
+
...(labelled ? { role: 'region' } : {}),
|
|
545
584
|
id: state.ids.panel,
|
|
546
|
-
'aria-labelledby': state.ids.trigger,
|
|
585
|
+
...(labelled ? { 'aria-labelledby': state.ids.trigger } : {}),
|
|
547
586
|
...(state.on ? {} : { hidden: UNTIL_FOUND }),
|
|
548
587
|
};
|
|
549
588
|
}
|
|
550
589
|
|
|
590
|
+
/** True when a region of this kind is a labelled `role=region`. */
|
|
591
|
+
export function hasLabelledRegion(kind: CompositeKind): boolean {
|
|
592
|
+
return kind === CompositeKind.Accordion;
|
|
593
|
+
}
|
|
594
|
+
|
|
551
595
|
export function accordionPanelAttributes(state: AccordionItemState): DomAttributes {
|
|
552
596
|
return state.on ? { [ON_ATTRIBUTE]: '' } : {};
|
|
553
597
|
}
|