@vectoriox/iox-ui 5.84.0 → 5.86.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 +47 -3
- package/composite-core/index.d.ts +93 -15
- package/design-core/index.d.ts +2013 -187
- package/fesm2022/vectoriox-iox-ui-actions-core.mjs +153 -70
- package/fesm2022/vectoriox-iox-ui-actions-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-composite-core.mjs +307 -39
- package/fesm2022/vectoriox-iox-ui-composite-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-design-core.mjs +2222 -85
- package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-forms-core.mjs +27 -4
- package/fesm2022/vectoriox-iox-ui-forms-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs +250 -12
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-scene-core.mjs +101 -5
- package/fesm2022/vectoriox-iox-ui-scene-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui.mjs +1545 -78
- package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
- package/forms-core/index.d.ts +23 -4
- package/generation-core/index.d.ts +13 -0
- package/index.d.ts +563 -62
- package/package.json +1 -1
- package/scene-core/index.d.ts +32 -4
- package/src/actions-core/action.model.ts +1 -1
- package/src/actions-core/dialog-motion.ts +25 -0
- package/src/actions-core/dialog-opener.ts +9 -3
- package/src/actions-core/element-actions.ts +25 -1
- package/src/actions-core/popover-opener.ts +50 -0
- package/src/actions-core/public-api.ts +1 -0
- package/src/composite-core/header.ts +25 -0
- package/src/composite-core/public-api.ts +1 -0
- package/src/composite-core/slider-runtime.ts +82 -0
- package/src/composite-core/slider.ts +194 -27
- package/src/composite-core/structure.ts +14 -9
- package/src/design-core/flavor-spec.generated.ts +2145 -85
- package/src/design-core/header-behaviour.ts +76 -0
- package/src/design-core/public-api.ts +1 -0
- package/src/design-core/theme-color.ts +21 -5
- package/src/forms-core/submit-lifecycle.ts +30 -3
- package/src/generation-core/baseline-roles.ts +1 -0
- package/src/generation-core/compose-site.ts +8 -0
- package/src/generation-core/node-baselines.generated.ts +231 -11
- package/src/generation-core/patterns.ts +2 -0
- package/src/generation-core/site-spec.generated.ts +8 -0
- package/src/scene-core/scene.ts +18 -0
- package/src/scene-core/scroll-driver.ts +83 -6
- package/styles/iox-design.css +431 -1
- package/styles/iox-forms.css +29 -4
package/package.json
CHANGED
package/scene-core/index.d.ts
CHANGED
|
@@ -70,8 +70,19 @@ interface SceneFrame {
|
|
|
70
70
|
declare enum SceneAttribute {
|
|
71
71
|
Root = "data-iox-scene",
|
|
72
72
|
Tracks = "data-iox-tracks",
|
|
73
|
-
PhoneTracks = "data-iox-tracks-phone"
|
|
73
|
+
PhoneTracks = "data-iox-tracks-phone",
|
|
74
|
+
/** A root's stage that is not a Scene's `scene.stage` part: the slider a scroll plays (premium M4). */
|
|
75
|
+
Stage = "data-iox-scene-stage"
|
|
74
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* A SLIDER THE SCROLL PLAYS (premium components M4 — M1.3's held gallery). Its progress rests on each picture and changes
|
|
79
|
+
* only over the middle share of each step, so every picture arrives and stays a moment while the page holds it: `p` is
|
|
80
|
+
* the scroll's progress through the runway (0–1), `n` the slides; the answer is Swiper's progress (0–1).
|
|
81
|
+
*/
|
|
82
|
+
declare const SLIDER_HOLD_SHARE = 0.2;
|
|
83
|
+
/** Page px of scroll per picture, as a share of the screen's height (the runway the renderers reserve). */
|
|
84
|
+
declare const SLIDER_SCROLL_STEP_SCREENS = 0.55;
|
|
85
|
+
declare function heldProgress(p: number, n: number): number;
|
|
75
86
|
/** At or under this stage width a layer plays its phone tracks. The `mobile` breakpoint's width. */
|
|
76
87
|
declare const SCENE_PHONE_MAX_WIDTH = 767;
|
|
77
88
|
/**
|
|
@@ -287,6 +298,11 @@ declare function buildSceneRecipe(create: CompositeNodeFactory, words?: Partial<
|
|
|
287
298
|
* - Reads come first in a frame and writes after, and a write only ever touches compositor properties.
|
|
288
299
|
*/
|
|
289
300
|
declare const SCENE_DAMPING = 9;
|
|
301
|
+
/**
|
|
302
|
+
* Dispatched (bubbling) by something the driver plays that arrived after the driver last looked — a Swiper booted on a
|
|
303
|
+
* slider's stage. Never a DOM `scroll`: nothing that follows the driver may dispatch one (SC2).
|
|
304
|
+
*/
|
|
305
|
+
declare const SCENE_WAKE_EVENT = "iox-scene-wake";
|
|
290
306
|
/** One frame of the damping: frame-rate independent, so 60 Hz and 120 Hz settle in the same time. */
|
|
291
307
|
declare function dampToward(current: number, target: number, seconds: number, rate?: number): number;
|
|
292
308
|
/** Where a root stands: scrolled `local` page px into its `travel`. `zoom` undoes the canvas's scale. */
|
|
@@ -321,9 +337,12 @@ declare class ScrollDriver {
|
|
|
321
337
|
private frame;
|
|
322
338
|
private lastTime;
|
|
323
339
|
private readonly wakeUp;
|
|
340
|
+
private readonly onWakeEvent;
|
|
324
341
|
constructor(options?: ScrollDriverOptions);
|
|
325
|
-
/** Drive a Scene's root. Idempotent: adding a root again re-reads it. */
|
|
326
|
-
add(root: HTMLElement):
|
|
342
|
+
/** Drive a Scene's root. Idempotent: adding a root again re-reads it. False when another driver holds it. */
|
|
343
|
+
add(root: HTMLElement): boolean;
|
|
344
|
+
/** True when this driver holds `root`. */
|
|
345
|
+
holds(root: HTMLElement): boolean;
|
|
327
346
|
remove(root: HTMLElement): void;
|
|
328
347
|
/** Read a root's layers again now, keeping where it stands: an editor changed their tracks. */
|
|
329
348
|
reread(root: HTMLElement): void;
|
|
@@ -366,6 +385,15 @@ declare class ScrollDriver {
|
|
|
366
385
|
/** A page's Scenes, driven by one driver; null when it holds none, or when the visitor asked for less motion (the
|
|
367
386
|
* page's own CSS then lets each Scene flow). The live site's door. */
|
|
368
387
|
declare function attachScenes(container: HTMLElement, options?: ScrollDriverOptions): ScrollDriver | null;
|
|
388
|
+
/**
|
|
389
|
+
* The Scene roots that belong to `elements` (each, and what it holds) — what one runtime instance owns: the page's nodes,
|
|
390
|
+
* or the globals'. Never a whole container that also holds another instance's Scenes (premium M4: the globals' instance
|
|
391
|
+
* scanned the app root and drove the page's Scenes a second time).
|
|
392
|
+
*/
|
|
393
|
+
declare function sceneRootsIn(elements: Iterable<Element>): HTMLElement[];
|
|
394
|
+
/** The live site's door for a runtime that owns some nodes: their Scenes on one driver (null when none, or under reduced
|
|
395
|
+
* motion — the page's own CSS then lets each Scene flow). A root another driver holds stays with it. */
|
|
396
|
+
declare function attachSceneRoots(roots: ReadonlyArray<HTMLElement>, options?: ScrollDriverOptions): ScrollDriver | null;
|
|
369
397
|
|
|
370
398
|
/**
|
|
371
399
|
* Text into pieces, in the browser only (premium sites SC3, Motion v2 §3c). A saved node keeps its plain text and the
|
|
@@ -416,5 +444,5 @@ declare function revealStagger(groups: number, by: TextRevealBy): number;
|
|
|
416
444
|
*/
|
|
417
445
|
declare function playTextReveal(el: HTMLElement, by: TextRevealBy, timing: KeyframeAnimationOptions): Animation | null;
|
|
418
446
|
|
|
419
|
-
export { LIGHT_UP_PIECE_CAP, REVEAL_PIECE_CAP, SCENE_BUDGET, SCENE_DAMPING, SCENE_FILM_SMOOTH, SCENE_FLOW_RULES, SCENE_LENGTH, SCENE_MAX_SCREENS, SCENE_MIN_SCREENS, SCENE_PHONE_LENGTH, SCENE_PHONE_MAX_WIDTH, SCENE_REDUCED_MOTION_CSS, SCENE_REVEAL_DEFAULT, SCENE_STAGE_HEIGHT, SCENE_TRACK_DEFS, SCENE_WORDS, SCENE_WORD_DIM, SceneAttribute, SceneProp, SceneRevealFrom, ScrollDriver, TextRevealBy, applySceneFirstFrame, attachScenes, buildSceneRecipe, colourAt, dampToward, lineIndexes, movedSceneKey, normalizeTracks, playTextReveal, revealClipPath, revealStagger, sceneBudget, sceneColourKeyAt, sceneDisplayValue, sceneFilmDensity, sceneFirstFrame, sceneFrame, sceneFrameDeclarations, sceneHasKeyAt, sceneHeightForScreens, sceneKeyPosition, sceneLayersOf, sceneNodeAttributes, scenePlayed, sceneRevealFromOf, sceneRootOnPath, sceneScreensOf, sceneScrollState, sceneTrackDef, sceneTrackSpan, sceneWordOpacity, sceneWordsLit, scenesIn, splitText, syncSceneAttributes, textPieces, valueAt, withSceneKey, withScenePlayback, withSceneRevealFrom, withoutSceneKey, withoutScenePlayback };
|
|
447
|
+
export { LIGHT_UP_PIECE_CAP, REVEAL_PIECE_CAP, SCENE_BUDGET, SCENE_DAMPING, SCENE_FILM_SMOOTH, SCENE_FLOW_RULES, SCENE_LENGTH, SCENE_MAX_SCREENS, SCENE_MIN_SCREENS, SCENE_PHONE_LENGTH, SCENE_PHONE_MAX_WIDTH, SCENE_REDUCED_MOTION_CSS, SCENE_REVEAL_DEFAULT, SCENE_STAGE_HEIGHT, SCENE_TRACK_DEFS, SCENE_WAKE_EVENT, SCENE_WORDS, SCENE_WORD_DIM, SLIDER_HOLD_SHARE, SLIDER_SCROLL_STEP_SCREENS, SceneAttribute, SceneProp, SceneRevealFrom, ScrollDriver, TextRevealBy, applySceneFirstFrame, attachSceneRoots, attachScenes, buildSceneRecipe, colourAt, dampToward, heldProgress, lineIndexes, movedSceneKey, normalizeTracks, playTextReveal, revealClipPath, revealStagger, sceneBudget, sceneColourKeyAt, sceneDisplayValue, sceneFilmDensity, sceneFirstFrame, sceneFrame, sceneFrameDeclarations, sceneHasKeyAt, sceneHeightForScreens, sceneKeyPosition, sceneLayersOf, sceneNodeAttributes, scenePlayed, sceneRevealFromOf, sceneRootOnPath, sceneRootsIn, sceneScreensOf, sceneScrollState, sceneTrackDef, sceneTrackSpan, sceneWordOpacity, sceneWordsLit, scenesIn, splitText, syncSceneAttributes, textPieces, valueAt, withSceneKey, withScenePlayback, withSceneRevealFrom, withoutSceneKey, withoutScenePlayback };
|
|
420
448
|
export type { SceneBudget, SceneFilmDensity, SceneFrame, SceneFrameOptions, SceneFrameState, SceneKey, SceneLayerNode, SceneNodeSource, SceneTrack, SceneTrackDef, SceneTreeNode, SceneWords, ScrollDriverOptions, SplitText, SplitTextOptions, TextPiece };
|
|
@@ -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' | 'item' | 'form' | 'data' | 'consent' | 'service';
|
|
14
|
+
| 'motion' | 'visibility' | 'navigate' | 'dialog' | 'popover' | 'notice' | 'item' | 'form' | 'data' | 'consent' | 'service';
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* What a step does inside the live builder canvas (the canvas RUNS interactions):
|
|
@@ -103,3 +103,28 @@ export function closeDialogAnimated(el: DialogElementLike, options: ExitMotionOp
|
|
|
103
103
|
export function dialogTogglesClosed(el: Pick<DialogElementLike, 'open' | 'hasAttribute'>): boolean {
|
|
104
104
|
return el.open && !el.hasAttribute(DIALOG_CLOSING_ATTRIBUTE);
|
|
105
105
|
}
|
|
106
|
+
|
|
107
|
+
/** The element-style marker a styled dialog carries (design-core ELEMENT_STYLE_MARKER_CLASS — actions-core does not
|
|
108
|
+
* import design-core). */
|
|
109
|
+
const STYLED_DIALOG_CLASS = 'iox-es';
|
|
110
|
+
|
|
111
|
+
export interface FocusableSheet {
|
|
112
|
+
classList: { contains(token: string): boolean };
|
|
113
|
+
querySelector(selector: string): unknown;
|
|
114
|
+
hasAttribute(name: string): boolean;
|
|
115
|
+
setAttribute(name: string, value: string): void;
|
|
116
|
+
focus(options?: FocusOptions): void;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* A styled dialog a visitor just opened takes focus on its SHEET (premium components M4 — M1.1's carry-over). A bare
|
|
121
|
+
* `showModal()` focuses its first control, and a phone can ring it after a tap; the sheet never rings (iox-design.css) and
|
|
122
|
+
* the first Tab moves to that control, with its ring. A dialog with an author's `[autofocus]` keeps it; a dialog saved
|
|
123
|
+
* before element styles keeps the browser's behaviour. True when the sheet took focus.
|
|
124
|
+
*/
|
|
125
|
+
export function focusDialogSheet(dialog: FocusableSheet | null | undefined): boolean {
|
|
126
|
+
if (!dialog || !dialog.classList.contains(STYLED_DIALOG_CLASS) || dialog.querySelector('[autofocus]')) return false;
|
|
127
|
+
if (!dialog.hasAttribute('tabindex')) dialog.setAttribute('tabindex', '-1');
|
|
128
|
+
dialog.focus({ preventScroll: true });
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
@@ -56,18 +56,24 @@ export interface OpenerNodeLike {
|
|
|
56
56
|
|
|
57
57
|
const SELF = 'self';
|
|
58
58
|
|
|
59
|
-
/** The
|
|
60
|
-
|
|
59
|
+
/** The node id the element's click opens through one of `openerTypes` (its first enabled such action), or null —
|
|
60
|
+
* the ONE reading every opener kind shares (a dialog's, a popover's). */
|
|
61
|
+
export function openedTargetId(node: OpenerNodeLike | null | undefined, openerTypes: ReadonlySet<string>): string | null {
|
|
61
62
|
for (const ix of node?.interactions ?? []) {
|
|
62
63
|
if (ix?.trigger !== DIALOG_OPENER_TRIGGER) continue;
|
|
63
64
|
for (const action of ix.actions ?? []) {
|
|
64
|
-
if (!action || action.disabled === true || !
|
|
65
|
+
if (!action || action.disabled === true || !openerTypes.has(String(action.type))) continue;
|
|
65
66
|
if (action.target && action.target !== SELF) return action.target;
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
69
|
return null;
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
/** The Dialog node id the element's click opens (its first enabled opener action), or null. */
|
|
73
|
+
export function openedDialogId(node: OpenerNodeLike | null | undefined): string | null {
|
|
74
|
+
return openedTargetId(node, DIALOG_OPENER_ACTION_TYPES);
|
|
75
|
+
}
|
|
76
|
+
|
|
71
77
|
/**
|
|
72
78
|
* The opener attributes for a node: `{}` when its click opens no dialog, else
|
|
73
79
|
* `aria-haspopup="dialog"`, `aria-controls="iox-dialog-<id>"` and `aria-expanded` (`false` unless
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AccordionRole, CompositeItemAction, DisclosureRole, SegmentedRole, SLIDE_TO_ROW, TabsRole } from '@vectoriox/iox-ui/composite-core';
|
|
2
2
|
|
|
3
3
|
import { ActionDescriptor } from './action.model';
|
|
4
|
+
import { POPOVER_NODE_TYPE } from './popover-opener';
|
|
4
5
|
|
|
5
6
|
/** The items an item action may target: an accordion's item, or a disclosure (its own one item). */
|
|
6
7
|
const ITEM_TARGET_ROLES = [AccordionRole.Item, DisclosureRole.Disclosure];
|
|
@@ -72,6 +73,26 @@ export const ELEMENT_ACTION_DESCRIPTORS: readonly ActionDescriptor[] = Object.fr
|
|
|
72
73
|
{ type: 'toggleDialog', labelKey: 'builder.actions.toggleDialog', kind: 'instant', group: 'dialog', builderBehaviour: 'run',
|
|
73
74
|
target: { required: true, types: ['Dialog'] } },
|
|
74
75
|
|
|
76
|
+
// ── Popover (premium M5, N4): an anchored, NON-modal panel beside the element that opens it. The canvas RUNS them,
|
|
77
|
+
// like the dialog actions — a popover you cannot open from its button is a popover you cannot design.
|
|
78
|
+
{ type: 'openPopover', labelKey: 'builder.actions.openPopover', kind: 'instant', group: 'popover', builderBehaviour: 'run',
|
|
79
|
+
target: { required: true, types: [POPOVER_NODE_TYPE] } },
|
|
80
|
+
{ type: 'closePopover', labelKey: 'builder.actions.closePopover', kind: 'instant', group: 'popover', builderBehaviour: 'run',
|
|
81
|
+
target: { required: true, types: [POPOVER_NODE_TYPE] } },
|
|
82
|
+
{ type: 'togglePopover', labelKey: 'builder.actions.togglePopover', kind: 'instant', group: 'popover', builderBehaviour: 'run',
|
|
83
|
+
target: { required: true, types: [POPOVER_NODE_TYPE] } },
|
|
84
|
+
|
|
85
|
+
// ── Notice (premium M5, N3): a toast at an edge of the screen — the author's words, never the visitor's. The canvas
|
|
86
|
+
// RUNS it inside the page's frame.
|
|
87
|
+
{ type: 'showToast', labelKey: 'builder.actions.showToast', kind: 'instant', group: 'notice', builderBehaviour: 'run',
|
|
88
|
+
params: [
|
|
89
|
+
{ name: 'text', type: 'string', label: 'builder.actions.params.toastText' },
|
|
90
|
+
{ name: 'edge', type: 'string', label: 'builder.actions.params.toastEdge', options: [
|
|
91
|
+
{ value: 'bottom', labelKey: 'builder.actions.params.toastBottom' },
|
|
92
|
+
{ value: 'top', labelKey: 'builder.actions.params.toastTop' },
|
|
93
|
+
] },
|
|
94
|
+
] },
|
|
95
|
+
|
|
75
96
|
// ── Item (CC2.1 — composite T14): drive an accordion item or a disclosure from anywhere ("See shipping"
|
|
76
97
|
// opens FAQ item 3). The canvas RUNS them, like the dialog actions: it is the same runtime, and an item you
|
|
77
98
|
// cannot open from the button is a button you cannot design.
|
|
@@ -147,6 +168,9 @@ export const SERVER_ELEMENT_ACTION_TYPES: ReadonlySet<string> =
|
|
|
147
168
|
export const BEHAVIOUR_ACTION_TYPES: ReadonlySet<string> =
|
|
148
169
|
new Set(['navigate', 'scrollTo', 'submitForm', 'copyToClipboard']);
|
|
149
170
|
|
|
171
|
+
/** The toast action's dispatch set (premium M5) — the engines branch on it beside DIALOG / POPOVER. */
|
|
172
|
+
export const TOAST_ACTION_TYPES: ReadonlySet<string> = new Set(['showToast']);
|
|
173
|
+
|
|
150
174
|
/** The item actions' dispatch set (T14) and what each does to its item — the engines branch on it beside
|
|
151
175
|
* DIALOG/BEHAVIOUR, and composite-core `runCompositeItemAction` runs it on the target item's own runtime. */
|
|
152
176
|
export const ITEM_ACTION_TYPES: Readonly<Record<string, CompositeItemAction>> = Object.freeze({
|
|
@@ -172,7 +196,7 @@ export const CONSENT_ACTION_TYPES: ReadonlySet<string> =
|
|
|
172
196
|
|
|
173
197
|
/** Picker order — most-used groups first, services last (they are the shop window, not the shop). */
|
|
174
198
|
export const ELEMENT_ACTION_GROUP_ORDER: readonly string[] =
|
|
175
|
-
Object.freeze(['motion', 'visibility', 'navigate', 'dialog', 'item', 'form', 'data', 'consent', 'service']);
|
|
199
|
+
Object.freeze(['motion', 'visibility', 'navigate', 'dialog', 'popover', 'notice', 'item', 'form', 'data', 'consent', 'service']);
|
|
176
200
|
|
|
177
201
|
/** The catalog, grouped and ordered for the picker — one derivation, no hand-kept second list. */
|
|
178
202
|
export function groupedElementActions(): ReadonlyArray<{ group: string; actions: ActionDescriptor[] }> {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { OpenerAttribute, OpenerNodeLike, openedTargetId } from './dialog-opener';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What an element that OPENS a popover says about itself (premium components M5, N4 — the APG disclosure pattern over a
|
|
5
|
+
* non-modal dialog): `aria-haspopup="dialog"`, `aria-controls` = the popover's DOM id, and `aria-expanded` — `false` in
|
|
6
|
+
* the server render, kept in step by the runtime. ONE decision for every renderer, read the way a dialog opener is
|
|
7
|
+
* (`openedTargetId`); the popover's DOM id comes from `popoverDomId`, the one place that names it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** The Popover node type. */
|
|
11
|
+
export const POPOVER_NODE_TYPE = 'Popover';
|
|
12
|
+
|
|
13
|
+
/** The actions that open a popover (close-only actions do not make an opener). */
|
|
14
|
+
export const POPOVER_OPENER_ACTION_TYPES: ReadonlySet<string> = new Set(['openPopover', 'togglePopover']);
|
|
15
|
+
|
|
16
|
+
/** Every action that opens, closes or toggles a popover — what the engines dispatch. */
|
|
17
|
+
export const POPOVER_ACTION_TYPE_SET: ReadonlySet<string> = new Set(['openPopover', 'closePopover', 'togglePopover']);
|
|
18
|
+
|
|
19
|
+
const POPOVER_ID_PREFIX = 'iox-popover-';
|
|
20
|
+
|
|
21
|
+
/** The DOM id of a Popover node's panel — what an opener's `aria-controls` names. */
|
|
22
|
+
export function popoverDomId(nodeId: string): string {
|
|
23
|
+
return `${POPOVER_ID_PREFIX}${nodeId}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The node id a popover DOM id names, or null. */
|
|
27
|
+
export function nodeIdOfPopoverDomId(domId: string | null | undefined): string | null {
|
|
28
|
+
return domId && domId.startsWith(POPOVER_ID_PREFIX) ? domId.slice(POPOVER_ID_PREFIX.length) : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The Popover node id the element's click opens, or null. */
|
|
32
|
+
export function openedPopoverId(node: OpenerNodeLike | null | undefined): string | null {
|
|
33
|
+
return openedTargetId(node, POPOVER_OPENER_ACTION_TYPES);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The opener attributes for a node: `{}` when its click opens no popover. */
|
|
37
|
+
export function popoverOpenerAttributes(node: OpenerNodeLike | null | undefined, open = false): Record<string, string> {
|
|
38
|
+
const popoverId = openedPopoverId(node);
|
|
39
|
+
if (!popoverId) return {};
|
|
40
|
+
return {
|
|
41
|
+
[OpenerAttribute.HasPopup]: 'dialog',
|
|
42
|
+
[OpenerAttribute.Controls]: popoverDomId(popoverId),
|
|
43
|
+
[OpenerAttribute.Expanded]: open ? 'true' : 'false',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The selector of every opener of one popover (by its DOM id). */
|
|
48
|
+
export function popoverOpenersSelector(popoverDomIdValue: string): string {
|
|
49
|
+
return `[${OpenerAttribute.Controls}="${popoverDomIdValue}"]`;
|
|
50
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { HeaderBehaviour, HeaderEdge, headerBehaviourOf, headerEdgeOf, headerStaysAtTop, HeaderTrait } from '@vectoriox/iox-ui/design-core';
|
|
2
|
+
|
|
3
|
+
import { CompositeNode, readProp, readStyle, writeProp, writeStyles } from './composite-node';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Set how a header behaves as the page scrolls (premium components T2, M4) — ONE edit for the builder's Header section
|
|
7
|
+
* and the composer (generation-core), on either node shape. A header that stays at the top is the containing block of its
|
|
8
|
+
* edge: the live site makes it sticky (iox-design.css), and a header stored `static` becomes `relative` — what the canvas,
|
|
9
|
+
* which forces a node's own position, draws it with. Nothing else of the author's is touched.
|
|
10
|
+
*/
|
|
11
|
+
export function setHeaderBehaviour(node: CompositeNode, behaviour: HeaderBehaviour | `${HeaderBehaviour}`, edge: HeaderEdge | `${HeaderEdge}` | null): void {
|
|
12
|
+
const b = headerBehaviourOf(behaviour);
|
|
13
|
+
writeProp(node, HeaderTrait.Behaviour, b);
|
|
14
|
+
writeProp(node, HeaderTrait.Edge, headerEdgeOf(edge) ?? '');
|
|
15
|
+
const position = readStyle(node, 'position');
|
|
16
|
+
if (headerStaysAtTop(b) && (position === undefined || position === null || position === '' || position === 'static')) writeStyles(node, { position: 'relative' });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The header's behaviour and edge as stored (Scrolls / none for a header that never chose). */
|
|
20
|
+
export function headerBehaviourOfNode(node: CompositeNode | null | undefined): { behaviour: HeaderBehaviour; edge: HeaderEdge | null } {
|
|
21
|
+
return {
|
|
22
|
+
behaviour: headerBehaviourOf(node ? readProp(node, HeaderTrait.Behaviour) : undefined),
|
|
23
|
+
edge: headerEdgeOf(node ? readProp(node, HeaderTrait.Edge) : undefined),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -178,3 +178,85 @@ export function attachCarouselA11y(swiper: CarouselSwiper, options: CarouselA11y
|
|
|
178
178
|
carouselOf(swiper.el)?.destroy();
|
|
179
179
|
return new CarouselA11y(swiper, options);
|
|
180
180
|
}
|
|
181
|
+
|
|
182
|
+
// ── The bar's words (premium components M4, M1.3) ──────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
/** Where the Caption and the Count parts get their words: generated content (`attr()`), never the element's text — so
|
|
185
|
+
* the canvas's Text component and the runtime never write the same node. */
|
|
186
|
+
export const SLIDER_TEXT_ATTRIBUTE = 'data-iox-text';
|
|
187
|
+
/** A slide's caption, written by every renderer (iox-ui `nodeA11yAttributes`, SLIDE_CAPTION_ATTRIBUTE). */
|
|
188
|
+
const CAPTION_ATTRIBUTE = 'data-iox-caption';
|
|
189
|
+
/** Held for a frame while the caption changes, so its new words fade in (iox-design.css). */
|
|
190
|
+
export const SLIDER_SWAPPING_ATTRIBUTE = 'data-iox-swapping';
|
|
191
|
+
const BAR_RUNTIME_KEY = '__ioxSliderBar';
|
|
192
|
+
|
|
193
|
+
/** What the caption says for a slide: its own caption, else its picture's description, else nothing. */
|
|
194
|
+
export function sliderCaptionOf(slide: Element | null | undefined): string {
|
|
195
|
+
if (!slide) return '';
|
|
196
|
+
const holder = slide.hasAttribute(CAPTION_ATTRIBUTE) ? slide : slide.querySelector(`[${CAPTION_ATTRIBUTE}]`);
|
|
197
|
+
const own = holder?.getAttribute(CAPTION_ATTRIBUTE)?.trim();
|
|
198
|
+
if (own) return own;
|
|
199
|
+
return slide.querySelector('img[alt]')?.getAttribute('alt')?.trim() ?? '';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** "2 / 5" — the slide in view and how many there are; nothing for an empty slider. */
|
|
203
|
+
export function sliderCountText(index: number, total: number): string {
|
|
204
|
+
if (!(total > 0)) return '';
|
|
205
|
+
const i = Math.min(total - 1, Math.max(0, Math.floor(Number.isFinite(index) ? index : 0)));
|
|
206
|
+
return `${i + 1} / ${total}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The parts of a running Swiper the bar's runtime reads. */
|
|
210
|
+
export interface BarSwiper {
|
|
211
|
+
el: HTMLElement;
|
|
212
|
+
slides: HTMLElement[];
|
|
213
|
+
activeIndex: number;
|
|
214
|
+
realIndex?: number;
|
|
215
|
+
destroyed?: boolean;
|
|
216
|
+
on(event: string, handler: (...args: unknown[]) => void): void;
|
|
217
|
+
off(event: string, handler: (...args: unknown[]) => void): void;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Keep the bar's Caption and Count on the slide in view — the canvas, Preview and both engine paths (bootSlider). The
|
|
222
|
+
* caption changes behind a short fade (the first one does not); the count reads left to right in every language.
|
|
223
|
+
* Returns the stop; a slider with neither part gets a no-op.
|
|
224
|
+
*/
|
|
225
|
+
export function attachSliderBar(swiper: BarSwiper): () => void {
|
|
226
|
+
((swiper.el as unknown as Record<string, () => void>)[BAR_RUNTIME_KEY])?.();
|
|
227
|
+
const caption = sliderPartElement(swiper.el, SliderRole.Caption);
|
|
228
|
+
const count = sliderPartElement(swiper.el, SliderRole.Count);
|
|
229
|
+
if (!caption && !count) return () => { /* nothing to keep */ };
|
|
230
|
+
const view = swiper.el.ownerDocument?.defaultView;
|
|
231
|
+
const raf = (fn: () => void) => (view?.requestAnimationFrame ? view.requestAnimationFrame(() => view.requestAnimationFrame(fn)) : setTimeout(fn, 32));
|
|
232
|
+
let first = true;
|
|
233
|
+
const sync = () => {
|
|
234
|
+
if (swiper.destroyed) return;
|
|
235
|
+
const slides = swiper.slides ?? [];
|
|
236
|
+
if (count) {
|
|
237
|
+
count.setAttribute('dir', 'ltr');
|
|
238
|
+
count.setAttribute(SLIDER_TEXT_ATTRIBUTE, sliderCountText(swiper.realIndex ?? swiper.activeIndex, slides.length));
|
|
239
|
+
}
|
|
240
|
+
if (caption) {
|
|
241
|
+
const words = sliderCaptionOf(slides[swiper.activeIndex]);
|
|
242
|
+
if (caption.getAttribute(SLIDER_TEXT_ATTRIBUTE) !== words) {
|
|
243
|
+
if (!first) {
|
|
244
|
+
caption.setAttribute(SLIDER_SWAPPING_ATTRIBUTE, '');
|
|
245
|
+
raf(() => caption.removeAttribute(SLIDER_SWAPPING_ATTRIBUTE));
|
|
246
|
+
}
|
|
247
|
+
caption.setAttribute(SLIDER_TEXT_ATTRIBUTE, words);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
first = false;
|
|
251
|
+
};
|
|
252
|
+
swiper.on('slideChange', sync);
|
|
253
|
+
swiper.on('slidesLengthChange', sync);
|
|
254
|
+
sync();
|
|
255
|
+
const stop = () => {
|
|
256
|
+
swiper.off('slideChange', sync);
|
|
257
|
+
swiper.off('slidesLengthChange', sync);
|
|
258
|
+
delete (swiper.el as unknown as Record<string, unknown>)[BAR_RUNTIME_KEY];
|
|
259
|
+
};
|
|
260
|
+
(swiper.el as unknown as Record<string, () => void>)[BAR_RUNTIME_KEY] = stop;
|
|
261
|
+
return stop;
|
|
262
|
+
}
|