ux4g-components-web 1.4.0 → 1.5.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/README.md +76 -0
- package/dist/__tests__/css-bundle.integration.test.d.ts +11 -0
- package/dist/__tests__/css-bundle.integration.test.js +1102 -0
- package/dist/__tests__/css-bundle.phase10.property.test.d.ts +9 -0
- package/dist/__tests__/css-bundle.phase10.property.test.js +64 -0
- package/dist/__tests__/css-bundle.phase5.property.test.d.ts +9 -0
- package/dist/__tests__/css-bundle.phase5.property.test.js +126 -0
- package/dist/__tests__/css-bundle.phase6.property.test.d.ts +9 -0
- package/dist/__tests__/css-bundle.phase6.property.test.js +73 -0
- package/dist/__tests__/css-bundle.phase7.property.test.d.ts +9 -0
- package/dist/__tests__/css-bundle.phase7.property.test.js +76 -0
- package/dist/__tests__/css-bundle.phase8.property.test.d.ts +9 -0
- package/dist/__tests__/css-bundle.phase8.property.test.js +67 -0
- package/dist/__tests__/css-bundle.phase9.property.test.d.ts +9 -0
- package/dist/__tests__/css-bundle.phase9.property.test.js +93 -0
- package/dist/__tests__/css-bundle.property.test.d.ts +14 -0
- package/dist/__tests__/css-bundle.property.test.js +393 -0
- package/dist/__tests__/dom-generators.determinism.property.test.d.ts +1 -0
- package/dist/__tests__/dom-generators.determinism.property.test.js +71 -0
- package/dist/__tests__/dom-generators.id.property.test.d.ts +1 -0
- package/dist/__tests__/dom-generators.id.property.test.js +99 -0
- package/dist/__tests__/dom-generators.otp.property.test.d.ts +1 -0
- package/dist/__tests__/dom-generators.property.test.d.ts +1 -0
- package/dist/__tests__/dom-generators.property.test.js +205 -0
- package/dist/__tests__/dom-generators.states.property.test.d.ts +1 -0
- package/dist/__tests__/dom-generators.table.property.test.d.ts +1 -0
- package/dist/__tests__/dom-generators.tier1.property.test.d.ts +1 -0
- package/dist/__tests__/dom-generators.tier1.property.test.js +403 -0
- package/dist/__tests__/dom-generators.validation.property.test.d.ts +1 -0
- package/dist/__tests__/dom-generators.validation.property.test.js +327 -0
- package/dist/__tests__/megamenu.classbuilder.property.test.d.ts +1 -0
- package/dist/__tests__/megamenu.classbuilder.property.test.js +88 -0
- package/dist/__tests__/smoke.test.d.ts +1 -0
- package/dist/__tests__/smoke.test.js +65 -0
- package/dist/__tests__/types.phase10.property.test.d.ts +1 -0
- package/dist/__tests__/types.phase10.property.test.js +166 -0
- package/dist/__tests__/types.phase10.test.d.ts +1 -0
- package/dist/__tests__/types.phase10.test.js +76 -0
- package/dist/__tests__/types.phase3.property.test.d.ts +1 -0
- package/dist/__tests__/types.phase3.property.test.js +83 -0
- package/dist/__tests__/types.phase3.test.d.ts +1 -0
- package/dist/__tests__/types.phase3.test.js +76 -0
- package/dist/__tests__/types.phase4.property.test.d.ts +1 -0
- package/dist/__tests__/types.phase4.property.test.js +119 -0
- package/dist/__tests__/types.phase4.test.d.ts +1 -0
- package/dist/__tests__/types.phase4.test.js +70 -0
- package/dist/__tests__/types.phase5.property.test.d.ts +1 -0
- package/dist/__tests__/types.phase5.property.test.js +120 -0
- package/dist/__tests__/types.phase5.test.d.ts +1 -0
- package/dist/__tests__/types.phase5.test.js +64 -0
- package/dist/__tests__/types.phase6.property.test.d.ts +1 -0
- package/dist/__tests__/types.phase6.property.test.js +189 -0
- package/dist/__tests__/types.phase6.test.d.ts +1 -0
- package/dist/__tests__/types.phase6.test.js +121 -0
- package/dist/__tests__/types.phase7.property.test.d.ts +1 -0
- package/dist/__tests__/types.phase7.property.test.js +217 -0
- package/dist/__tests__/types.phase7.test.d.ts +1 -0
- package/dist/__tests__/types.phase7.test.js +106 -0
- package/dist/__tests__/types.phase8.property.test.d.ts +1 -0
- package/dist/__tests__/types.phase8.property.test.js +224 -0
- package/dist/__tests__/types.phase8.test.d.ts +1 -0
- package/dist/__tests__/types.phase8.test.js +114 -0
- package/dist/__tests__/types.phase9.property.test.d.ts +1 -0
- package/dist/__tests__/types.phase9.property.test.js +347 -0
- package/dist/__tests__/types.phase9.test.d.ts +1 -0
- package/dist/__tests__/types.phase9.test.js +226 -0
- package/dist/__tests__/types.restructure.property.test.d.ts +1 -0
- package/dist/__tests__/types.restructure.property.test.js +76 -0
- package/dist/__tests__/types.test.d.ts +1 -0
- package/dist/__tests__/types.test.js +175 -0
- package/dist/dom-generators/accordion.d.ts +23 -0
- package/dist/dom-generators/avatar.d.ts +19 -0
- package/dist/dom-generators/carousel.d.ts +20 -0
- package/dist/dom-generators/chip.d.ts +18 -0
- package/dist/dom-generators/combobox.d.ts +28 -0
- package/dist/dom-generators/date-picker.d.ts +19 -0
- package/dist/dom-generators/dom-generators/accordion.d.ts +21 -0
- package/dist/dom-generators/dom-generators/avatar.d.ts +17 -0
- package/dist/dom-generators/dom-generators/carousel.d.ts +19 -0
- package/dist/dom-generators/dom-generators/chip.d.ts +16 -0
- package/dist/dom-generators/dom-generators/combobox.d.ts +26 -0
- package/dist/dom-generators/dom-generators/date-picker.d.ts +18 -0
- package/dist/dom-generators/dom-generators/drawer.d.ts +17 -0
- package/dist/dom-generators/dom-generators/dropdown.d.ts +26 -0
- package/dist/dom-generators/dom-generators/file-upload.d.ts +20 -0
- package/dist/dom-generators/dom-generators/id-generator.d.ts +9 -0
- package/dist/dom-generators/dom-generators/index.d.ts +27 -0
- package/dist/dom-generators/dom-generators/modal.d.ts +19 -0
- package/dist/dom-generators/dom-generators/otp.d.ts +16 -0
- package/dist/dom-generators/dom-generators/popover.d.ts +17 -0
- package/dist/dom-generators/dom-generators/progress.d.ts +16 -0
- package/dist/dom-generators/dom-generators/search.d.ts +20 -0
- package/dist/dom-generators/dom-generators/stepper.d.ts +21 -0
- package/dist/dom-generators/dom-generators/table.d.ts +23 -0
- package/dist/dom-generators/dom-generators/tabs.d.ts +21 -0
- package/dist/dom-generators/dom-generators/time-picker.d.ts +18 -0
- package/dist/dom-generators/dom-generators/tooltip.d.ts +17 -0
- package/dist/dom-generators/dom-generators/types.d.ts +27 -0
- package/dist/dom-generators/dom-generators/validate.d.ts +20 -0
- package/dist/dom-generators/drawer.d.ts +19 -0
- package/dist/dom-generators/dropdown.d.ts +28 -0
- package/dist/dom-generators/file-upload.d.ts +22 -0
- package/dist/dom-generators/id-generator.d.ts +9 -0
- package/dist/dom-generators/index.bundled.d.ts +654 -0
- package/dist/dom-generators/index.cjs +2029 -0
- package/dist/dom-generators/index.d.ts +27 -0
- package/dist/dom-generators/index.mjs +2001 -0
- package/dist/dom-generators/modal.d.ts +21 -0
- package/dist/dom-generators/otp.d.ts +18 -0
- package/dist/dom-generators/popover.d.ts +19 -0
- package/dist/dom-generators/progress.d.ts +18 -0
- package/dist/dom-generators/search.d.ts +22 -0
- package/dist/dom-generators/stepper.d.ts +23 -0
- package/dist/dom-generators/table.d.ts +25 -0
- package/dist/dom-generators/tabs.d.ts +23 -0
- package/dist/dom-generators/time-picker.d.ts +19 -0
- package/dist/dom-generators/tooltip.d.ts +19 -0
- package/dist/dom-generators/types.d.ts +155 -0
- package/dist/dom-generators/validate.d.ts +20 -0
- package/dist/runtime/bootstrap.js +59 -0
- package/dist/runtime/index.js +55 -0
- package/dist/types.d.ts +155 -0
- package/dist/types.js +552 -0
- package/package.json +12 -2
|
@@ -0,0 +1,2029 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ux4g-components-web/src/dom-generators/types.ts
|
|
4
|
+
// Framework-agnostic structural descriptor for DOM generation.
|
|
5
|
+
/**
|
|
6
|
+
* Error thrown when a DOM Generator receives an invalid prop value.
|
|
7
|
+
* Provides detailed context about which component, prop, and value caused the issue.
|
|
8
|
+
*/
|
|
9
|
+
class DOMGeneratorValidationError extends Error {
|
|
10
|
+
constructor(componentName, propName, propValue, expectedType) {
|
|
11
|
+
super(`[UX4G DOM Generator] Invalid prop "${propName}" on ${componentName}: ` +
|
|
12
|
+
`received ${JSON.stringify(propValue)}, expected ${expectedType}`);
|
|
13
|
+
this.componentName = componentName;
|
|
14
|
+
this.propName = propName;
|
|
15
|
+
this.propValue = propValue;
|
|
16
|
+
this.expectedType = expectedType;
|
|
17
|
+
this.name = 'DOMGeneratorValidationError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ux4g-components-web/src/dom-generators/id-generator.ts
|
|
22
|
+
// Monotonically incrementing ID generator for document-unique element IDs.
|
|
23
|
+
let counter = 0;
|
|
24
|
+
/**
|
|
25
|
+
* Generates a unique ID by combining the given prefix with an incrementing counter.
|
|
26
|
+
* Produces IDs like `ux4g-dropdown-1`, `ux4g-tab-panel-2`, etc.
|
|
27
|
+
*/
|
|
28
|
+
function generateId(prefix) {
|
|
29
|
+
return `${prefix}-${++counter}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Resets the ID counter to zero. Useful for testing to ensure deterministic output.
|
|
33
|
+
*/
|
|
34
|
+
function resetIdCounter() {
|
|
35
|
+
counter = 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ux4g-components-web/src/dom-generators/validate.ts
|
|
39
|
+
// Prop validation helpers for DOM Generator functions.
|
|
40
|
+
/**
|
|
41
|
+
* Throws if the value is null or undefined.
|
|
42
|
+
*/
|
|
43
|
+
function validateRequired(componentName, propName, value) {
|
|
44
|
+
if (value === null || value === undefined) {
|
|
45
|
+
throw new DOMGeneratorValidationError(componentName, propName, value, 'a non-null/undefined value');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Throws if typeof value does not match the expected type string.
|
|
50
|
+
*/
|
|
51
|
+
function validateType(componentName, propName, value, expectedType) {
|
|
52
|
+
if (typeof value !== expectedType) {
|
|
53
|
+
throw new DOMGeneratorValidationError(componentName, propName, value, expectedType);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Throws if the value is not a member of the allowed values set.
|
|
58
|
+
*/
|
|
59
|
+
function validateEnum(componentName, propName, value, allowedValues) {
|
|
60
|
+
if (!allowedValues.includes(value)) {
|
|
61
|
+
throw new DOMGeneratorValidationError(componentName, propName, value, `one of [${allowedValues.map((v) => JSON.stringify(v)).join(', ')}]`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Throws if the numeric value is outside the given [min, max] range (inclusive).
|
|
66
|
+
*/
|
|
67
|
+
function validateRange(componentName, propName, value, min, max) {
|
|
68
|
+
if (typeof value !== 'number' || value < min || value > max) {
|
|
69
|
+
throw new DOMGeneratorValidationError(componentName, propName, value, `a number between ${min} and ${max}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Throws if the value is not an array.
|
|
74
|
+
*/
|
|
75
|
+
function validateArray(componentName, propName, value) {
|
|
76
|
+
if (!Array.isArray(value)) {
|
|
77
|
+
throw new DOMGeneratorValidationError(componentName, propName, value, 'an array');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Maps variant → CSS class
|
|
82
|
+
function buildAvatarClasses(avatarType, size, extra) {
|
|
83
|
+
if (avatarType === 'group') {
|
|
84
|
+
return extra ? `ux4g-avatar-group ${extra}` : 'ux4g-avatar-group';
|
|
85
|
+
}
|
|
86
|
+
const parts = ['ux4g-avatar', `ux4g-avatar-${avatarType}`];
|
|
87
|
+
if (size)
|
|
88
|
+
parts.push(`ux4g-avatar-${size}`);
|
|
89
|
+
return parts.join(' ');
|
|
90
|
+
}
|
|
91
|
+
function buildChipClasses(chipType, size, active, extra) {
|
|
92
|
+
// xs is only valid for input chips; filter and choice clamp to sm
|
|
93
|
+
const effectiveSize = (chipType !== 'input' && size === 'xs') ? 'sm' : size;
|
|
94
|
+
const parts = [`ux4g-${chipType}-chip-${effectiveSize}`];
|
|
95
|
+
if (active)
|
|
96
|
+
parts.push('active');
|
|
97
|
+
return parts.join(' ');
|
|
98
|
+
}
|
|
99
|
+
function buildDropdownClasses(type, mode, size, state, open, extra) {
|
|
100
|
+
const parts = [
|
|
101
|
+
'ux4g-dropdown',
|
|
102
|
+
`ux4g-dropdown-${type}`,
|
|
103
|
+
`ux4g-dropdown-${mode}`,
|
|
104
|
+
`ux4g-dropdown-${size}`,
|
|
105
|
+
`ux4g-dropdown-${state}`,
|
|
106
|
+
];
|
|
107
|
+
if (open)
|
|
108
|
+
parts.push('is-open');
|
|
109
|
+
if (extra)
|
|
110
|
+
parts.push(extra);
|
|
111
|
+
return parts.join(' ');
|
|
112
|
+
}
|
|
113
|
+
function buildComboboxClasses(type, size, state, open, extra) {
|
|
114
|
+
const parts = [
|
|
115
|
+
'ux4g-combobox',
|
|
116
|
+
`ux4g-combobox-${type}`,
|
|
117
|
+
`ux4g-combobox-${size}`,
|
|
118
|
+
`ux4g-combobox-${state}`,
|
|
119
|
+
];
|
|
120
|
+
if (open)
|
|
121
|
+
parts.push('is-open');
|
|
122
|
+
return parts.join(' ');
|
|
123
|
+
}
|
|
124
|
+
function buildModalBackdropClasses(opacity, blur, open, extra) {
|
|
125
|
+
const parts = ['ux4g-modal-backdrop', `ux4g-modal-backdrop-${opacity}`];
|
|
126
|
+
if (blur)
|
|
127
|
+
parts.push('ux4g-modal-backdrop-blur');
|
|
128
|
+
if (open)
|
|
129
|
+
parts.push('is-open');
|
|
130
|
+
return parts.join(' ');
|
|
131
|
+
}
|
|
132
|
+
function buildModalBoxClasses(size, centerContent, extra) {
|
|
133
|
+
const parts = ['ux4g-modal-box', `ux4g-modal-${size}`];
|
|
134
|
+
return parts.join(' ');
|
|
135
|
+
}
|
|
136
|
+
function buildSearchContainerClasses(size, extra) {
|
|
137
|
+
const base = `ux4g-search-container ux4g-search-${size}`;
|
|
138
|
+
return extra ? `${base} ${extra}` : base;
|
|
139
|
+
}
|
|
140
|
+
function buildTableClasses(size, divider, zebra, interactive, sortable, resizable, headerBrand, extra) {
|
|
141
|
+
const parts = ['ux4g-table', `ux4g-table-${size}`];
|
|
142
|
+
if (divider === 'column')
|
|
143
|
+
parts.push('ux4g-table-column-dividers');
|
|
144
|
+
else if (divider === 'none')
|
|
145
|
+
parts.push('ux4g-table-no-row-dividers');
|
|
146
|
+
if (zebra === 'rows')
|
|
147
|
+
parts.push('ux4g-table-zebra-rows');
|
|
148
|
+
else if (zebra === 'cols')
|
|
149
|
+
parts.push('ux4g-table-zebra-cols');
|
|
150
|
+
if (interactive)
|
|
151
|
+
parts.push('ux4g-table-interactive');
|
|
152
|
+
if (sortable)
|
|
153
|
+
parts.push('ux4g-table-sortable');
|
|
154
|
+
return parts.join(' ');
|
|
155
|
+
}
|
|
156
|
+
function buildPopoverClasses(placement, show, extra) {
|
|
157
|
+
const parts = ['ux4g-popover', `ux4g-popover-${placement}`];
|
|
158
|
+
if (show)
|
|
159
|
+
parts.push('show');
|
|
160
|
+
return parts.join(' ');
|
|
161
|
+
}
|
|
162
|
+
function buildTooltipClasses(placement, size, extra) {
|
|
163
|
+
const parts = ['ux4g-tooltip', `ux4g-tooltip-${placement}`, `ux4g-tooltip-${size}`];
|
|
164
|
+
return parts.join(' ');
|
|
165
|
+
}
|
|
166
|
+
function buildTabClasses(variant, size, vertical, extra) {
|
|
167
|
+
const parts = ['ux4g-tab', `ux4g-tab-${variant}`, `ux4g-tab-${size}`];
|
|
168
|
+
if (vertical)
|
|
169
|
+
parts.push('ux4g-tab-vertical');
|
|
170
|
+
return parts.join(' ');
|
|
171
|
+
}
|
|
172
|
+
function buildAccordionClasses(arrowPosition, variant, extra) {
|
|
173
|
+
const parts = ['ux4g-accordion'];
|
|
174
|
+
if (arrowPosition === 'right')
|
|
175
|
+
parts.push('ux4g-accordion-arrow-right');
|
|
176
|
+
else if (arrowPosition === 'left')
|
|
177
|
+
parts.push('ux4g-accordion-arrow-left');
|
|
178
|
+
if (variant === 'bordered')
|
|
179
|
+
parts.push('ux4g-accordion-bordered');
|
|
180
|
+
return parts.join(' ');
|
|
181
|
+
}
|
|
182
|
+
function buildStepperClasses(orientation, alignment, variant, size, extra) {
|
|
183
|
+
const parts = ['ux4g-stepper'];
|
|
184
|
+
if (orientation === 'vertical') {
|
|
185
|
+
parts.push('ux4g-stepper-vertical');
|
|
186
|
+
}
|
|
187
|
+
if (variant === 'bottom-line')
|
|
188
|
+
parts.push('ux4g-stepper-bottom-line');
|
|
189
|
+
else if (variant === 'bottom-line-fill') {
|
|
190
|
+
parts.push('ux4g-stepper-bottom-line');
|
|
191
|
+
parts.push('ux4g-stepper-bottom-line-fill');
|
|
192
|
+
}
|
|
193
|
+
else if (variant === 'mobile')
|
|
194
|
+
parts.push('ux4g-stepper-mobile');
|
|
195
|
+
else if (variant === 'progress')
|
|
196
|
+
parts.push('ux4g-stepper-progress');
|
|
197
|
+
if (size === 's')
|
|
198
|
+
parts.push('ux4g-stepper-s');
|
|
199
|
+
return parts.join(' ');
|
|
200
|
+
}
|
|
201
|
+
function buildDrawerClasses(placement, open, extra) {
|
|
202
|
+
const parts = ['ux4g-drawer', `ux4g-drawer-${placement}`];
|
|
203
|
+
if (open)
|
|
204
|
+
parts.push('ux4g-drawer-open');
|
|
205
|
+
return parts.join(' ');
|
|
206
|
+
}
|
|
207
|
+
function buildDateTimePickerClasses(mode, extra) {
|
|
208
|
+
const base = mode === 'date' ? 'ux4g-date-picker-container' : 'ux4g-time-picker-container';
|
|
209
|
+
return extra ? `${base} ${extra}` : base;
|
|
210
|
+
}
|
|
211
|
+
function buildOtpInputClasses(state, extra) {
|
|
212
|
+
const parts = ['ux4g-otp'];
|
|
213
|
+
if (state && state !== 'default')
|
|
214
|
+
parts.push(`ux4g-otp-${state}`);
|
|
215
|
+
return parts.join(' ');
|
|
216
|
+
}
|
|
217
|
+
function buildFileUploadClasses(state, extra) {
|
|
218
|
+
const parts = ['ux4g-upload'];
|
|
219
|
+
if (state)
|
|
220
|
+
parts.push(`ux4g-upload-state-${state}`);
|
|
221
|
+
return parts.join(' ');
|
|
222
|
+
}
|
|
223
|
+
function buildProgressIndicatorClasses(type, extra) {
|
|
224
|
+
const base = type === 'bar' ? 'ux4g-progress-bar' : 'ux4g-progress-circle';
|
|
225
|
+
return base;
|
|
226
|
+
}
|
|
227
|
+
// -- Carousel -----------------------------------------------------------------
|
|
228
|
+
function buildCarouselClasses(extra) {
|
|
229
|
+
return extra ? `ux4g-carousel ${extra}` : 'ux4g-carousel';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ux4g-components-web/src/dom-generators/modal.ts
|
|
233
|
+
// Framework-agnostic DOM Generator for the Modal component.
|
|
234
|
+
/**
|
|
235
|
+
* Generates a complete Modal DOM descriptor tree.
|
|
236
|
+
*
|
|
237
|
+
* Produces a backdrop element and a modal dialog box with `role="dialog"`,
|
|
238
|
+
* `aria-modal="true"`, an optional close button with `data-ux4g-modal-close`,
|
|
239
|
+
* and an optional title heading. Compatible with Runtime_JS
|
|
240
|
+
* MutationObserver-based initialization.
|
|
241
|
+
*/
|
|
242
|
+
function generateModalDOM(props) {
|
|
243
|
+
const { open = false, size = 'm', title, closable = true, backdropOpacity = '50', backdropBlur = false, } = props;
|
|
244
|
+
// Generate unique IDs
|
|
245
|
+
const modalId = generateId('ux4g-modal');
|
|
246
|
+
const titleId = generateId('ux4g-modal-title');
|
|
247
|
+
// Build backdrop element
|
|
248
|
+
const backdropClasses = buildModalBackdropClasses(backdropOpacity, backdropBlur, open);
|
|
249
|
+
const backdropNode = {
|
|
250
|
+
tag: 'div',
|
|
251
|
+
props: {
|
|
252
|
+
className: backdropClasses,
|
|
253
|
+
},
|
|
254
|
+
children: [],
|
|
255
|
+
};
|
|
256
|
+
// Build modal box children
|
|
257
|
+
const modalBoxChildren = [];
|
|
258
|
+
// Close button (if closable)
|
|
259
|
+
if (closable) {
|
|
260
|
+
const closeButton = {
|
|
261
|
+
tag: 'button',
|
|
262
|
+
props: {
|
|
263
|
+
className: 'ux4g-modal-close',
|
|
264
|
+
type: 'button',
|
|
265
|
+
'data-ux4g-modal-close': 'true',
|
|
266
|
+
'aria-label': 'Close',
|
|
267
|
+
},
|
|
268
|
+
children: [],
|
|
269
|
+
};
|
|
270
|
+
modalBoxChildren.push(closeButton);
|
|
271
|
+
}
|
|
272
|
+
// Title heading (if title provided)
|
|
273
|
+
if (title) {
|
|
274
|
+
const titleNode = {
|
|
275
|
+
tag: 'h2',
|
|
276
|
+
props: {
|
|
277
|
+
className: 'ux4g-modal-title',
|
|
278
|
+
id: titleId,
|
|
279
|
+
},
|
|
280
|
+
children: [title],
|
|
281
|
+
};
|
|
282
|
+
modalBoxChildren.push(titleNode);
|
|
283
|
+
}
|
|
284
|
+
// Build modal box element
|
|
285
|
+
const modalBoxClasses = buildModalBoxClasses(size);
|
|
286
|
+
const modalBoxProps = {
|
|
287
|
+
className: modalBoxClasses,
|
|
288
|
+
id: modalId,
|
|
289
|
+
role: 'dialog',
|
|
290
|
+
'aria-modal': 'true',
|
|
291
|
+
};
|
|
292
|
+
// Link title for accessible labelling
|
|
293
|
+
if (title) {
|
|
294
|
+
modalBoxProps['aria-labelledby'] = titleId;
|
|
295
|
+
}
|
|
296
|
+
const modalBoxNode = {
|
|
297
|
+
tag: 'div',
|
|
298
|
+
props: modalBoxProps,
|
|
299
|
+
children: modalBoxChildren,
|
|
300
|
+
};
|
|
301
|
+
// Build root wrapper containing backdrop and modal box
|
|
302
|
+
const rootNode = {
|
|
303
|
+
tag: 'div',
|
|
304
|
+
props: {
|
|
305
|
+
className: 'ux4g-modal',
|
|
306
|
+
},
|
|
307
|
+
children: [backdropNode, modalBoxNode],
|
|
308
|
+
};
|
|
309
|
+
return rootNode;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ux4g-components-web/src/dom-generators/carousel.ts
|
|
313
|
+
// Framework-agnostic DOM Generator for the Carousel component.
|
|
314
|
+
/**
|
|
315
|
+
* Generates a complete Carousel DOM descriptor tree.
|
|
316
|
+
*
|
|
317
|
+
* Produces a root element with `aria-roledescription="carousel"`, slide items with
|
|
318
|
+
* `role="group"` and `aria-roledescription="slide"`, navigation buttons with
|
|
319
|
+
* `data-ux4g-carousel-prev` and `data-ux4g-carousel-next` for Runtime_JS, and
|
|
320
|
+
* indicator dots for slide navigation.
|
|
321
|
+
*/
|
|
322
|
+
function generateCarouselDOM(props) {
|
|
323
|
+
const { slides, activeIndex = 0, showIndicators = true, showNavigation = true, } = props;
|
|
324
|
+
// Validate required props
|
|
325
|
+
validateArray('Carousel', 'slides', slides);
|
|
326
|
+
// Generate unique IDs for each slide
|
|
327
|
+
const slideIds = slides.map(() => generateId('ux4g-carousel-slide'));
|
|
328
|
+
// Build slide items
|
|
329
|
+
const slideItems = slides.map((slide, index) => {
|
|
330
|
+
const isActive = index === activeIndex;
|
|
331
|
+
return {
|
|
332
|
+
tag: 'div',
|
|
333
|
+
props: {
|
|
334
|
+
className: 'ux4g-carousel-slide',
|
|
335
|
+
id: slideIds[index],
|
|
336
|
+
role: 'group',
|
|
337
|
+
'aria-roledescription': 'slide',
|
|
338
|
+
'aria-label': slide.label || `Slide ${index + 1}`,
|
|
339
|
+
...(isActive ? {} : { 'aria-hidden': 'true' }),
|
|
340
|
+
},
|
|
341
|
+
children: [slide.content],
|
|
342
|
+
};
|
|
343
|
+
});
|
|
344
|
+
// Build slide container
|
|
345
|
+
const slideContainer = {
|
|
346
|
+
tag: 'div',
|
|
347
|
+
props: {
|
|
348
|
+
className: 'ux4g-carousel-slides',
|
|
349
|
+
},
|
|
350
|
+
children: slideItems,
|
|
351
|
+
};
|
|
352
|
+
// Build root children
|
|
353
|
+
const rootChildren = [slideContainer];
|
|
354
|
+
// Add navigation buttons if enabled
|
|
355
|
+
if (showNavigation) {
|
|
356
|
+
const prevButton = {
|
|
357
|
+
tag: 'button',
|
|
358
|
+
props: {
|
|
359
|
+
className: 'ux4g-carousel-prev',
|
|
360
|
+
type: 'button',
|
|
361
|
+
'data-ux4g-carousel-prev': 'true',
|
|
362
|
+
'aria-label': 'Previous slide',
|
|
363
|
+
},
|
|
364
|
+
children: [],
|
|
365
|
+
};
|
|
366
|
+
const nextButton = {
|
|
367
|
+
tag: 'button',
|
|
368
|
+
props: {
|
|
369
|
+
className: 'ux4g-carousel-next',
|
|
370
|
+
type: 'button',
|
|
371
|
+
'data-ux4g-carousel-next': 'true',
|
|
372
|
+
'aria-label': 'Next slide',
|
|
373
|
+
},
|
|
374
|
+
children: [],
|
|
375
|
+
};
|
|
376
|
+
rootChildren.push(prevButton, nextButton);
|
|
377
|
+
}
|
|
378
|
+
// Add indicator dots if enabled
|
|
379
|
+
if (showIndicators) {
|
|
380
|
+
const indicatorDots = slides.map((_, index) => {
|
|
381
|
+
const isActive = index === activeIndex;
|
|
382
|
+
return {
|
|
383
|
+
tag: 'button',
|
|
384
|
+
props: {
|
|
385
|
+
className: 'ux4g-carousel-indicator',
|
|
386
|
+
type: 'button',
|
|
387
|
+
'aria-label': `Go to slide ${index + 1}`,
|
|
388
|
+
...(isActive ? { 'aria-current': 'true' } : {}),
|
|
389
|
+
},
|
|
390
|
+
children: [],
|
|
391
|
+
};
|
|
392
|
+
});
|
|
393
|
+
const indicatorContainer = {
|
|
394
|
+
tag: 'div',
|
|
395
|
+
props: {
|
|
396
|
+
className: 'ux4g-carousel-indicators',
|
|
397
|
+
role: 'tablist',
|
|
398
|
+
},
|
|
399
|
+
children: indicatorDots,
|
|
400
|
+
};
|
|
401
|
+
rootChildren.push(indicatorContainer);
|
|
402
|
+
}
|
|
403
|
+
// Build root element
|
|
404
|
+
const rootNode = {
|
|
405
|
+
tag: 'div',
|
|
406
|
+
props: {
|
|
407
|
+
className: buildCarouselClasses(),
|
|
408
|
+
'aria-roledescription': 'carousel',
|
|
409
|
+
'aria-label': 'Carousel',
|
|
410
|
+
},
|
|
411
|
+
children: rootChildren,
|
|
412
|
+
};
|
|
413
|
+
return rootNode;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ux4g-components-web/src/dom-generators/dropdown.ts
|
|
417
|
+
// Framework-agnostic DOM Generator for the Dropdown component.
|
|
418
|
+
/**
|
|
419
|
+
* Generates a complete Dropdown DOM descriptor tree.
|
|
420
|
+
*
|
|
421
|
+
* Produces a trigger button with `data-ux4g-dropdown-toggle`, a menu container
|
|
422
|
+
* with `role="listbox"`, and option items with `role="option"` and `aria-selected`.
|
|
423
|
+
* Compatible with Runtime_JS MutationObserver-based initialization.
|
|
424
|
+
*/
|
|
425
|
+
function generateDropdownDOM(props) {
|
|
426
|
+
const { options, value, placeholder = 'Select...', searchable = false, multiple = false, disabled = false, size = 'md', state = 'default', open = false, } = props;
|
|
427
|
+
// Validate required props
|
|
428
|
+
validateRequired('Dropdown', 'options', options);
|
|
429
|
+
validateArray('Dropdown', 'options', options);
|
|
430
|
+
// Generate unique IDs
|
|
431
|
+
const triggerId = generateId('ux4g-dropdown-trigger');
|
|
432
|
+
const menuId = generateId('ux4g-dropdown-menu');
|
|
433
|
+
// Determine the mode based on multiple prop
|
|
434
|
+
const mode = multiple ? 'multi' : 'single';
|
|
435
|
+
// Build CSS classes via Class Builder
|
|
436
|
+
const dropdownClasses = buildDropdownClasses('selection', mode, size, state, open, disabled ? 'ux4g-dropdown-disabled' : undefined);
|
|
437
|
+
// Determine display text for the trigger
|
|
438
|
+
const selectedValues = Array.isArray(value) ? value : value != null ? [value] : [];
|
|
439
|
+
const selectedLabels = options
|
|
440
|
+
.filter((opt) => selectedValues.includes(opt.value))
|
|
441
|
+
.map((opt) => opt.label);
|
|
442
|
+
const displayText = selectedLabels.length > 0 ? selectedLabels.join(', ') : placeholder;
|
|
443
|
+
// Build trigger button
|
|
444
|
+
const triggerNode = {
|
|
445
|
+
tag: 'button',
|
|
446
|
+
props: {
|
|
447
|
+
className: 'ux4g-dropdown-trigger',
|
|
448
|
+
id: triggerId,
|
|
449
|
+
'data-ux4g-dropdown-toggle': 'true',
|
|
450
|
+
'aria-haspopup': 'listbox',
|
|
451
|
+
'aria-expanded': String(open),
|
|
452
|
+
'aria-controls': menuId,
|
|
453
|
+
type: 'button',
|
|
454
|
+
...(disabled ? { 'aria-disabled': 'true', disabled: true } : {}),
|
|
455
|
+
},
|
|
456
|
+
children: [displayText],
|
|
457
|
+
};
|
|
458
|
+
// Build option items
|
|
459
|
+
const optionItems = options.map((opt) => {
|
|
460
|
+
const isSelected = selectedValues.includes(opt.value);
|
|
461
|
+
const optionProps = {
|
|
462
|
+
className: 'ux4g-dropdown-option',
|
|
463
|
+
role: 'option',
|
|
464
|
+
'aria-selected': String(isSelected),
|
|
465
|
+
'data-value': opt.value,
|
|
466
|
+
};
|
|
467
|
+
if (opt.disabled) {
|
|
468
|
+
optionProps['aria-disabled'] = 'true';
|
|
469
|
+
}
|
|
470
|
+
return {
|
|
471
|
+
tag: 'div',
|
|
472
|
+
props: optionProps,
|
|
473
|
+
children: [opt.label],
|
|
474
|
+
};
|
|
475
|
+
});
|
|
476
|
+
// Build menu children (search input if searchable, then options)
|
|
477
|
+
const menuChildren = [];
|
|
478
|
+
if (searchable) {
|
|
479
|
+
const searchInput = {
|
|
480
|
+
tag: 'input',
|
|
481
|
+
props: {
|
|
482
|
+
className: 'ux4g-dropdown-search',
|
|
483
|
+
type: 'text',
|
|
484
|
+
role: 'searchbox',
|
|
485
|
+
'aria-label': 'Filter options',
|
|
486
|
+
placeholder: 'Search...',
|
|
487
|
+
},
|
|
488
|
+
children: [],
|
|
489
|
+
};
|
|
490
|
+
menuChildren.push(searchInput);
|
|
491
|
+
}
|
|
492
|
+
menuChildren.push(...optionItems);
|
|
493
|
+
// Build menu container
|
|
494
|
+
const menuNode = {
|
|
495
|
+
tag: 'div',
|
|
496
|
+
props: {
|
|
497
|
+
className: 'ux4g-dropdown-menu',
|
|
498
|
+
id: menuId,
|
|
499
|
+
role: 'listbox',
|
|
500
|
+
'aria-multiselectable': multiple ? 'true' : undefined,
|
|
501
|
+
},
|
|
502
|
+
children: menuChildren,
|
|
503
|
+
};
|
|
504
|
+
// Build root container
|
|
505
|
+
const rootNode = {
|
|
506
|
+
tag: 'div',
|
|
507
|
+
props: {
|
|
508
|
+
className: dropdownClasses,
|
|
509
|
+
},
|
|
510
|
+
children: [triggerNode, menuNode],
|
|
511
|
+
};
|
|
512
|
+
return rootNode;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ux4g-components-web/src/dom-generators/tabs.ts
|
|
516
|
+
// Framework-agnostic DOM Generator for the Tabs component.
|
|
517
|
+
/**
|
|
518
|
+
* Generates a complete Tabs DOM descriptor tree.
|
|
519
|
+
*
|
|
520
|
+
* Produces a tablist container with `role="tablist"`, individual tabs with
|
|
521
|
+
* `role="tab"` and `aria-selected`, and corresponding panels with
|
|
522
|
+
* `role="tabpanel"` and `aria-labelledby`.
|
|
523
|
+
*/
|
|
524
|
+
function generateTabsDOM(props) {
|
|
525
|
+
const { tabs, activeIndex = 0, variant = 'underline', size = 'md', vertical = false, } = props;
|
|
526
|
+
// Validate required props
|
|
527
|
+
validateArray('Tabs', 'tabs', tabs);
|
|
528
|
+
// Generate paired IDs for each tab/panel
|
|
529
|
+
const tabIds = [];
|
|
530
|
+
const panelIds = [];
|
|
531
|
+
for (let i = 0; i < tabs.length; i++) {
|
|
532
|
+
tabIds.push(generateId('ux4g-tab'));
|
|
533
|
+
panelIds.push(generateId('ux4g-tab-panel'));
|
|
534
|
+
}
|
|
535
|
+
// Build tab elements
|
|
536
|
+
const tabNodes = tabs.map((tab, index) => {
|
|
537
|
+
const isActive = index === activeIndex;
|
|
538
|
+
const tabProps = {
|
|
539
|
+
className: 'ux4g-tab-item',
|
|
540
|
+
id: tabIds[index],
|
|
541
|
+
role: 'tab',
|
|
542
|
+
'aria-selected': String(isActive),
|
|
543
|
+
'aria-controls': panelIds[index],
|
|
544
|
+
tabIndex: isActive ? 0 : -1,
|
|
545
|
+
};
|
|
546
|
+
if (tab.disabled) {
|
|
547
|
+
tabProps['aria-disabled'] = 'true';
|
|
548
|
+
}
|
|
549
|
+
return {
|
|
550
|
+
tag: 'button',
|
|
551
|
+
props: tabProps,
|
|
552
|
+
children: [tab.label],
|
|
553
|
+
};
|
|
554
|
+
});
|
|
555
|
+
// Build tablist container
|
|
556
|
+
const tablistNode = {
|
|
557
|
+
tag: 'div',
|
|
558
|
+
props: {
|
|
559
|
+
className: 'ux4g-tablist',
|
|
560
|
+
role: 'tablist',
|
|
561
|
+
'aria-orientation': vertical ? 'vertical' : 'horizontal',
|
|
562
|
+
},
|
|
563
|
+
children: tabNodes,
|
|
564
|
+
};
|
|
565
|
+
// Build panel elements
|
|
566
|
+
const panelNodes = tabs.map((tab, index) => {
|
|
567
|
+
const isActive = index === activeIndex;
|
|
568
|
+
const panelProps = {
|
|
569
|
+
className: 'ux4g-tab-panel',
|
|
570
|
+
id: panelIds[index],
|
|
571
|
+
role: 'tabpanel',
|
|
572
|
+
'aria-labelledby': tabIds[index],
|
|
573
|
+
tabIndex: 0,
|
|
574
|
+
};
|
|
575
|
+
if (!isActive) {
|
|
576
|
+
panelProps['aria-hidden'] = 'true';
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
tag: 'div',
|
|
580
|
+
props: panelProps,
|
|
581
|
+
children: [tab.content],
|
|
582
|
+
};
|
|
583
|
+
});
|
|
584
|
+
// Build CSS classes via Class Builder
|
|
585
|
+
const rootClasses = buildTabClasses(variant, size, vertical);
|
|
586
|
+
// Build root container
|
|
587
|
+
const rootNode = {
|
|
588
|
+
tag: 'div',
|
|
589
|
+
props: {
|
|
590
|
+
className: rootClasses,
|
|
591
|
+
},
|
|
592
|
+
children: [tablistNode, ...panelNodes],
|
|
593
|
+
};
|
|
594
|
+
return rootNode;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ux4g-components-web/src/dom-generators/accordion.ts
|
|
598
|
+
// DOM Generator for the Accordion component.
|
|
599
|
+
/**
|
|
600
|
+
* Generates a complete Accordion DOM descriptor tree.
|
|
601
|
+
*
|
|
602
|
+
* Produces headers with `data-ux4g-accordion-toggle` for Runtime_JS,
|
|
603
|
+
* panels with unique IDs, aria-expanded on toggles, aria-controls
|
|
604
|
+
* referencing panel IDs, and role="region" on panels.
|
|
605
|
+
*/
|
|
606
|
+
function generateAccordionDOM(props) {
|
|
607
|
+
const { items, arrowPosition, variant } = props;
|
|
608
|
+
validateArray('Accordion', 'items', items);
|
|
609
|
+
const accordionItems = items.map((item) => {
|
|
610
|
+
const headerId = generateId('ux4g-accordion-header');
|
|
611
|
+
const panelId = generateId('ux4g-accordion-panel');
|
|
612
|
+
const isExpanded = item.expanded ?? false;
|
|
613
|
+
const headerButtonProps = {
|
|
614
|
+
className: 'ux4g-accordion-button',
|
|
615
|
+
id: headerId,
|
|
616
|
+
'data-ux4g-accordion-toggle': 'true',
|
|
617
|
+
'aria-expanded': String(isExpanded),
|
|
618
|
+
'aria-controls': panelId,
|
|
619
|
+
};
|
|
620
|
+
if (item.disabled) {
|
|
621
|
+
headerButtonProps['aria-disabled'] = 'true';
|
|
622
|
+
}
|
|
623
|
+
const headerButton = {
|
|
624
|
+
tag: 'button',
|
|
625
|
+
props: headerButtonProps,
|
|
626
|
+
children: [item.header],
|
|
627
|
+
};
|
|
628
|
+
const header = {
|
|
629
|
+
tag: 'div',
|
|
630
|
+
props: {
|
|
631
|
+
className: 'ux4g-accordion-header',
|
|
632
|
+
},
|
|
633
|
+
children: [headerButton],
|
|
634
|
+
};
|
|
635
|
+
const panel = {
|
|
636
|
+
tag: 'div',
|
|
637
|
+
props: {
|
|
638
|
+
className: 'ux4g-accordion-panel',
|
|
639
|
+
id: panelId,
|
|
640
|
+
role: 'region',
|
|
641
|
+
'aria-labelledby': headerId,
|
|
642
|
+
},
|
|
643
|
+
children: [item.content],
|
|
644
|
+
};
|
|
645
|
+
return {
|
|
646
|
+
tag: 'div',
|
|
647
|
+
props: {
|
|
648
|
+
className: 'ux4g-accordion-item',
|
|
649
|
+
},
|
|
650
|
+
children: [header, panel],
|
|
651
|
+
};
|
|
652
|
+
});
|
|
653
|
+
return {
|
|
654
|
+
tag: 'div',
|
|
655
|
+
props: {
|
|
656
|
+
className: buildAccordionClasses(arrowPosition, variant),
|
|
657
|
+
},
|
|
658
|
+
children: accordionItems,
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// ux4g-components-web/src/dom-generators/time-picker.ts
|
|
663
|
+
// Framework-agnostic DOM Generator for the TimePicker component.
|
|
664
|
+
/**
|
|
665
|
+
* Parses a time string in HH:mm format and returns hour and minute values.
|
|
666
|
+
* Returns defaults (0, 0) for invalid or missing values.
|
|
667
|
+
*/
|
|
668
|
+
function parseTimeValue(value) {
|
|
669
|
+
if (!value) {
|
|
670
|
+
return { hour: 0, minute: 0 };
|
|
671
|
+
}
|
|
672
|
+
const parts = value.split(':');
|
|
673
|
+
if (parts.length < 2) {
|
|
674
|
+
return { hour: 0, minute: 0 };
|
|
675
|
+
}
|
|
676
|
+
const hour = parseInt(parts[0], 10);
|
|
677
|
+
const minute = parseInt(parts[1], 10);
|
|
678
|
+
return {
|
|
679
|
+
hour: isNaN(hour) ? 0 : hour,
|
|
680
|
+
minute: isNaN(minute) ? 0 : minute,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Generates a complete TimePicker DOM descriptor tree.
|
|
685
|
+
*
|
|
686
|
+
* Produces hour, minute, and (for 12h format) period spinbutton elements
|
|
687
|
+
* with appropriate ARIA attributes: `role="spinbutton"`, `aria-valuemin`,
|
|
688
|
+
* `aria-valuemax`, `aria-valuenow`, and `aria-label`.
|
|
689
|
+
*
|
|
690
|
+
* The root element uses CSS classes from `buildDateTimePickerClasses('time')`.
|
|
691
|
+
*/
|
|
692
|
+
function generateTimePickerDOM(props) {
|
|
693
|
+
const { value, format = '24h', } = props;
|
|
694
|
+
const { hour, minute } = parseTimeValue(value);
|
|
695
|
+
// Generate unique IDs for each spinbutton
|
|
696
|
+
const hourId = generateId('ux4g-time-picker-hour');
|
|
697
|
+
const minuteId = generateId('ux4g-time-picker-minute');
|
|
698
|
+
// Determine hour range based on format
|
|
699
|
+
const is12h = format === '12h';
|
|
700
|
+
const hourMin = is12h ? 1 : 0;
|
|
701
|
+
const hourMax = is12h ? 12 : 23;
|
|
702
|
+
// Convert 24h hour to 12h display value
|
|
703
|
+
let displayHour = hour;
|
|
704
|
+
let periodValue = 0; // 0 = AM, 1 = PM
|
|
705
|
+
if (is12h) {
|
|
706
|
+
periodValue = hour >= 12 ? 1 : 0;
|
|
707
|
+
displayHour = hour % 12;
|
|
708
|
+
if (displayHour === 0) {
|
|
709
|
+
displayHour = 12;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
// Build hour spinbutton
|
|
713
|
+
const hourSpinbutton = {
|
|
714
|
+
tag: 'div',
|
|
715
|
+
props: {
|
|
716
|
+
className: 'ux4g-time-picker-hour',
|
|
717
|
+
id: hourId,
|
|
718
|
+
role: 'spinbutton',
|
|
719
|
+
tabIndex: 0,
|
|
720
|
+
'aria-valuemin': hourMin,
|
|
721
|
+
'aria-valuemax': hourMax,
|
|
722
|
+
'aria-valuenow': displayHour,
|
|
723
|
+
'aria-label': 'Hours',
|
|
724
|
+
},
|
|
725
|
+
children: [String(displayHour)],
|
|
726
|
+
};
|
|
727
|
+
// Build minute spinbutton
|
|
728
|
+
const minuteSpinbutton = {
|
|
729
|
+
tag: 'div',
|
|
730
|
+
props: {
|
|
731
|
+
className: 'ux4g-time-picker-minute',
|
|
732
|
+
id: minuteId,
|
|
733
|
+
role: 'spinbutton',
|
|
734
|
+
tabIndex: 0,
|
|
735
|
+
'aria-valuemin': 0,
|
|
736
|
+
'aria-valuemax': 59,
|
|
737
|
+
'aria-valuenow': minute,
|
|
738
|
+
'aria-label': 'Minutes',
|
|
739
|
+
},
|
|
740
|
+
children: [String(minute)],
|
|
741
|
+
};
|
|
742
|
+
// Build children array
|
|
743
|
+
const children = [hourSpinbutton, minuteSpinbutton];
|
|
744
|
+
// For 12h format, add period selector
|
|
745
|
+
if (is12h) {
|
|
746
|
+
const periodId = generateId('ux4g-time-picker-period');
|
|
747
|
+
const periodSpinbutton = {
|
|
748
|
+
tag: 'div',
|
|
749
|
+
props: {
|
|
750
|
+
className: 'ux4g-time-picker-period',
|
|
751
|
+
id: periodId,
|
|
752
|
+
role: 'spinbutton',
|
|
753
|
+
tabIndex: 0,
|
|
754
|
+
'aria-valuemin': 0,
|
|
755
|
+
'aria-valuemax': 1,
|
|
756
|
+
'aria-valuenow': periodValue,
|
|
757
|
+
'aria-label': 'Period',
|
|
758
|
+
},
|
|
759
|
+
children: [periodValue === 0 ? 'AM' : 'PM'],
|
|
760
|
+
};
|
|
761
|
+
children.push(periodSpinbutton);
|
|
762
|
+
}
|
|
763
|
+
// Build root element with class from buildDateTimePickerClasses('time')
|
|
764
|
+
const rootNode = {
|
|
765
|
+
tag: 'div',
|
|
766
|
+
props: {
|
|
767
|
+
className: buildDateTimePickerClasses('time'),
|
|
768
|
+
},
|
|
769
|
+
children,
|
|
770
|
+
};
|
|
771
|
+
return rootNode;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// ux4g-components-web/src/dom-generators/search.ts
|
|
775
|
+
// Framework-agnostic DOM Generator for the Search component.
|
|
776
|
+
/**
|
|
777
|
+
* Generates a complete Search DOM descriptor tree.
|
|
778
|
+
*
|
|
779
|
+
* Produces a search input with `role="searchbox"`, a clear button visible when
|
|
780
|
+
* there is a value, and a suggestions listbox with `role="listbox"`.
|
|
781
|
+
* Compatible with Runtime_JS MutationObserver-based initialization.
|
|
782
|
+
*/
|
|
783
|
+
function generateSearchDOM(props) {
|
|
784
|
+
const { value = '', placeholder = 'Search...', suggestions = [], showSuggestions = false, size = 'm', } = props;
|
|
785
|
+
// Generate unique IDs
|
|
786
|
+
const inputId = generateId('ux4g-search-input');
|
|
787
|
+
const suggestionsId = generateId('ux4g-search-suggestions');
|
|
788
|
+
// Build CSS classes via Class Builder
|
|
789
|
+
const containerClasses = buildSearchContainerClasses(size);
|
|
790
|
+
// Determine if suggestions should be visible
|
|
791
|
+
const hasSuggestions = suggestions.length > 0;
|
|
792
|
+
const suggestionsVisible = showSuggestions && hasSuggestions;
|
|
793
|
+
// Build search input
|
|
794
|
+
const inputProps = {
|
|
795
|
+
className: 'ux4g-search-input',
|
|
796
|
+
id: inputId,
|
|
797
|
+
role: 'searchbox',
|
|
798
|
+
'aria-label': 'Search',
|
|
799
|
+
placeholder,
|
|
800
|
+
type: 'text',
|
|
801
|
+
};
|
|
802
|
+
if (hasSuggestions) {
|
|
803
|
+
inputProps['aria-controls'] = suggestionsId;
|
|
804
|
+
}
|
|
805
|
+
inputProps['aria-expanded'] = String(suggestionsVisible);
|
|
806
|
+
if (value) {
|
|
807
|
+
inputProps['value'] = value;
|
|
808
|
+
}
|
|
809
|
+
const inputNode = {
|
|
810
|
+
tag: 'input',
|
|
811
|
+
props: inputProps,
|
|
812
|
+
children: [],
|
|
813
|
+
};
|
|
814
|
+
// Build clear button (visible when value has content)
|
|
815
|
+
const clearButtonNode = {
|
|
816
|
+
tag: 'button',
|
|
817
|
+
props: {
|
|
818
|
+
className: 'ux4g-search-clear',
|
|
819
|
+
type: 'button',
|
|
820
|
+
'aria-label': 'Clear search',
|
|
821
|
+
...(value ? {} : { 'aria-hidden': 'true' }),
|
|
822
|
+
},
|
|
823
|
+
children: ['×'],
|
|
824
|
+
};
|
|
825
|
+
// Build suggestion items
|
|
826
|
+
const suggestionItems = suggestions.map((suggestion) => ({
|
|
827
|
+
tag: 'div',
|
|
828
|
+
props: {
|
|
829
|
+
className: 'ux4g-search-suggestion',
|
|
830
|
+
role: 'option',
|
|
831
|
+
'data-value': suggestion.value,
|
|
832
|
+
},
|
|
833
|
+
children: [suggestion.label],
|
|
834
|
+
}));
|
|
835
|
+
// Build suggestions listbox
|
|
836
|
+
const suggestionsNode = {
|
|
837
|
+
tag: 'div',
|
|
838
|
+
props: {
|
|
839
|
+
className: 'ux4g-search-suggestions',
|
|
840
|
+
id: suggestionsId,
|
|
841
|
+
role: 'listbox',
|
|
842
|
+
'aria-label': 'Search suggestions',
|
|
843
|
+
...(!suggestionsVisible ? { 'aria-hidden': 'true' } : {}),
|
|
844
|
+
},
|
|
845
|
+
children: suggestionItems,
|
|
846
|
+
};
|
|
847
|
+
// Build root container
|
|
848
|
+
const rootNode = {
|
|
849
|
+
tag: 'div',
|
|
850
|
+
props: {
|
|
851
|
+
className: containerClasses,
|
|
852
|
+
},
|
|
853
|
+
children: [inputNode, clearButtonNode, suggestionsNode],
|
|
854
|
+
};
|
|
855
|
+
return rootNode;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// ux4g-components-web/src/dom-generators/drawer.ts
|
|
859
|
+
// Framework-agnostic DOM Generator for the Drawer component.
|
|
860
|
+
/**
|
|
861
|
+
* Generates a complete Drawer DOM descriptor tree.
|
|
862
|
+
*
|
|
863
|
+
* Produces a backdrop element and a drawer panel with `role="dialog"`,
|
|
864
|
+
* `aria-modal="true"`, an optional close button with `data-ux4g-drawer-close`,
|
|
865
|
+
* and an optional title heading. Compatible with Runtime_JS
|
|
866
|
+
* MutationObserver-based initialization.
|
|
867
|
+
*/
|
|
868
|
+
function generateDrawerDOM(props) {
|
|
869
|
+
const { open = false, placement = 'right', title, closable = true, } = props;
|
|
870
|
+
// Generate unique IDs
|
|
871
|
+
const drawerId = generateId('ux4g-drawer');
|
|
872
|
+
const titleId = generateId('ux4g-drawer-title');
|
|
873
|
+
// Build backdrop element
|
|
874
|
+
const backdropNode = {
|
|
875
|
+
tag: 'div',
|
|
876
|
+
props: {
|
|
877
|
+
className: 'ux4g-drawer-backdrop',
|
|
878
|
+
},
|
|
879
|
+
children: [],
|
|
880
|
+
};
|
|
881
|
+
// Build drawer panel children
|
|
882
|
+
const drawerPanelChildren = [];
|
|
883
|
+
// Close button (if closable)
|
|
884
|
+
if (closable) {
|
|
885
|
+
const closeButton = {
|
|
886
|
+
tag: 'button',
|
|
887
|
+
props: {
|
|
888
|
+
className: 'ux4g-drawer-close',
|
|
889
|
+
type: 'button',
|
|
890
|
+
'data-ux4g-drawer-close': 'true',
|
|
891
|
+
'aria-label': 'Close',
|
|
892
|
+
},
|
|
893
|
+
children: [],
|
|
894
|
+
};
|
|
895
|
+
drawerPanelChildren.push(closeButton);
|
|
896
|
+
}
|
|
897
|
+
// Title heading (if title provided)
|
|
898
|
+
if (title) {
|
|
899
|
+
const titleNode = {
|
|
900
|
+
tag: 'h2',
|
|
901
|
+
props: {
|
|
902
|
+
className: 'ux4g-drawer-title',
|
|
903
|
+
id: titleId,
|
|
904
|
+
},
|
|
905
|
+
children: [title],
|
|
906
|
+
};
|
|
907
|
+
drawerPanelChildren.push(titleNode);
|
|
908
|
+
}
|
|
909
|
+
// Build drawer panel element
|
|
910
|
+
const drawerPanelClasses = buildDrawerClasses(placement, open);
|
|
911
|
+
const drawerPanelProps = {
|
|
912
|
+
className: drawerPanelClasses,
|
|
913
|
+
id: drawerId,
|
|
914
|
+
role: 'dialog',
|
|
915
|
+
'aria-modal': 'true',
|
|
916
|
+
};
|
|
917
|
+
// Link title for accessible labelling
|
|
918
|
+
if (title) {
|
|
919
|
+
drawerPanelProps['aria-labelledby'] = titleId;
|
|
920
|
+
}
|
|
921
|
+
const drawerPanelNode = {
|
|
922
|
+
tag: 'div',
|
|
923
|
+
props: drawerPanelProps,
|
|
924
|
+
children: drawerPanelChildren,
|
|
925
|
+
};
|
|
926
|
+
// Build root wrapper containing backdrop + drawer panel
|
|
927
|
+
const rootNode = {
|
|
928
|
+
tag: 'div',
|
|
929
|
+
props: {
|
|
930
|
+
className: 'ux4g-drawer-wrapper',
|
|
931
|
+
},
|
|
932
|
+
children: [backdropNode, drawerPanelNode],
|
|
933
|
+
};
|
|
934
|
+
return rootNode;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// ux4g-components-web/src/dom-generators/combobox.ts
|
|
938
|
+
// Framework-agnostic DOM Generator for the Combobox component.
|
|
939
|
+
/**
|
|
940
|
+
* Generates a complete Combobox DOM descriptor tree.
|
|
941
|
+
*
|
|
942
|
+
* Produces an input with `role="combobox"` and `aria-autocomplete="list"`,
|
|
943
|
+
* a listbox with `role="listbox"`, and option items with `role="option"`.
|
|
944
|
+
* Supports `aria-activedescendant` pointing to the first selected option.
|
|
945
|
+
* Compatible with Runtime_JS MutationObserver-based initialization.
|
|
946
|
+
*/
|
|
947
|
+
function generateComboboxDOM(props) {
|
|
948
|
+
const { options, value, inputValue = '', placeholder = '', multiple = false, disabled = false, size = 'md', state = 'default', open = false, } = props;
|
|
949
|
+
// Validate required props
|
|
950
|
+
validateRequired('Combobox', 'options', options);
|
|
951
|
+
validateArray('Combobox', 'options', options);
|
|
952
|
+
// Generate unique IDs
|
|
953
|
+
const inputId = generateId('ux4g-combobox-input');
|
|
954
|
+
const listboxId = generateId('ux4g-combobox-listbox');
|
|
955
|
+
// Determine selected values
|
|
956
|
+
const selectedValues = Array.isArray(value) ? value : value != null ? [value] : [];
|
|
957
|
+
// Generate option IDs and find active descendant
|
|
958
|
+
const optionIds = options.map(() => generateId('ux4g-combobox-option'));
|
|
959
|
+
// Active descendant is the first selected option's ID (if any)
|
|
960
|
+
let activeDescendantId;
|
|
961
|
+
for (let i = 0; i < options.length; i++) {
|
|
962
|
+
if (selectedValues.includes(options[i].value)) {
|
|
963
|
+
activeDescendantId = optionIds[i];
|
|
964
|
+
break;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
// Determine combobox type based on multiple prop
|
|
968
|
+
const type = multiple ? 'multi' : 'single';
|
|
969
|
+
// Build CSS classes via Class Builder
|
|
970
|
+
const comboboxClasses = buildComboboxClasses(type, size, state, open);
|
|
971
|
+
// Build input element
|
|
972
|
+
const inputNode = {
|
|
973
|
+
tag: 'input',
|
|
974
|
+
props: {
|
|
975
|
+
className: 'ux4g-combobox-input',
|
|
976
|
+
id: inputId,
|
|
977
|
+
role: 'combobox',
|
|
978
|
+
'aria-autocomplete': 'list',
|
|
979
|
+
'aria-expanded': String(open),
|
|
980
|
+
'aria-controls': listboxId,
|
|
981
|
+
...(activeDescendantId ? { 'aria-activedescendant': activeDescendantId } : {}),
|
|
982
|
+
...(placeholder ? { placeholder } : {}),
|
|
983
|
+
...(inputValue ? { value: inputValue } : {}),
|
|
984
|
+
type: 'text',
|
|
985
|
+
...(disabled ? { 'aria-disabled': 'true', disabled: true } : {}),
|
|
986
|
+
},
|
|
987
|
+
children: [],
|
|
988
|
+
};
|
|
989
|
+
// Build option items
|
|
990
|
+
const optionItems = options.map((opt, index) => {
|
|
991
|
+
const isSelected = selectedValues.includes(opt.value);
|
|
992
|
+
const optionProps = {
|
|
993
|
+
className: 'ux4g-combobox-option',
|
|
994
|
+
id: optionIds[index],
|
|
995
|
+
role: 'option',
|
|
996
|
+
'aria-selected': String(isSelected),
|
|
997
|
+
'data-value': opt.value,
|
|
998
|
+
};
|
|
999
|
+
if (opt.disabled) {
|
|
1000
|
+
optionProps['aria-disabled'] = 'true';
|
|
1001
|
+
}
|
|
1002
|
+
return {
|
|
1003
|
+
tag: 'div',
|
|
1004
|
+
props: optionProps,
|
|
1005
|
+
children: [opt.label],
|
|
1006
|
+
};
|
|
1007
|
+
});
|
|
1008
|
+
// Build listbox container
|
|
1009
|
+
const listboxNode = {
|
|
1010
|
+
tag: 'div',
|
|
1011
|
+
props: {
|
|
1012
|
+
className: 'ux4g-combobox-listbox',
|
|
1013
|
+
id: listboxId,
|
|
1014
|
+
role: 'listbox',
|
|
1015
|
+
...(multiple ? { 'aria-multiselectable': 'true' } : {}),
|
|
1016
|
+
},
|
|
1017
|
+
children: optionItems,
|
|
1018
|
+
};
|
|
1019
|
+
// Build root container
|
|
1020
|
+
const rootNode = {
|
|
1021
|
+
tag: 'div',
|
|
1022
|
+
props: {
|
|
1023
|
+
className: comboboxClasses,
|
|
1024
|
+
},
|
|
1025
|
+
children: [inputNode, listboxNode],
|
|
1026
|
+
};
|
|
1027
|
+
return rootNode;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// ux4g-components-web/src/dom-generators/date-picker.ts
|
|
1031
|
+
// Framework-agnostic DOM Generator for the DatePicker component.
|
|
1032
|
+
/**
|
|
1033
|
+
* Returns the number of days in a given month/year.
|
|
1034
|
+
*/
|
|
1035
|
+
function getDaysInMonth(year, month) {
|
|
1036
|
+
return new Date(year, month + 1, 0).getDate();
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Returns the day of the week (0=Sun, 6=Sat) for the first day of the given month.
|
|
1040
|
+
*/
|
|
1041
|
+
function getFirstDayOfWeek(year, month) {
|
|
1042
|
+
return new Date(year, month, 1).getDay();
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Formats a date as a full human-readable string for aria-label (e.g., "January 15, 2024").
|
|
1046
|
+
*/
|
|
1047
|
+
function formatDateLabel(year, month, day, locale) {
|
|
1048
|
+
const date = new Date(year, month, day);
|
|
1049
|
+
try {
|
|
1050
|
+
return date.toLocaleDateString(locale, {
|
|
1051
|
+
year: 'numeric',
|
|
1052
|
+
month: 'long',
|
|
1053
|
+
day: 'numeric',
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
catch {
|
|
1057
|
+
// Fallback for invalid locale
|
|
1058
|
+
return date.toLocaleDateString('en-US', {
|
|
1059
|
+
year: 'numeric',
|
|
1060
|
+
month: 'long',
|
|
1061
|
+
day: 'numeric',
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Returns the month name for display in the header.
|
|
1067
|
+
*/
|
|
1068
|
+
function getMonthName(year, month, locale) {
|
|
1069
|
+
const date = new Date(year, month, 1);
|
|
1070
|
+
try {
|
|
1071
|
+
return date.toLocaleDateString(locale, { month: 'long' });
|
|
1072
|
+
}
|
|
1073
|
+
catch {
|
|
1074
|
+
return date.toLocaleDateString('en-US', { month: 'long' });
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
/**
|
|
1078
|
+
* Returns the abbreviated weekday names starting from Sunday.
|
|
1079
|
+
*/
|
|
1080
|
+
function getWeekdayHeaders(locale) {
|
|
1081
|
+
const weekdays = [];
|
|
1082
|
+
// Jan 4, 2024 is a Thursday — use a known Sunday: Jan 7, 2024
|
|
1083
|
+
const baseSunday = new Date(2024, 0, 7); // Sunday
|
|
1084
|
+
for (let i = 0; i < 7; i++) {
|
|
1085
|
+
const d = new Date(baseSunday);
|
|
1086
|
+
d.setDate(baseSunday.getDate() + i);
|
|
1087
|
+
try {
|
|
1088
|
+
weekdays.push(d.toLocaleDateString(locale, { weekday: 'short' }));
|
|
1089
|
+
}
|
|
1090
|
+
catch {
|
|
1091
|
+
weekdays.push(d.toLocaleDateString('en-US', { weekday: 'short' }));
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
return weekdays;
|
|
1095
|
+
}
|
|
1096
|
+
/**
|
|
1097
|
+
* Checks if a date string (YYYY-MM-DD) represents a date that is outside the min/max range.
|
|
1098
|
+
*/
|
|
1099
|
+
function isDateDisabled(year, month, day, min, max) {
|
|
1100
|
+
const dateValue = new Date(year, month, day).getTime();
|
|
1101
|
+
if (min) {
|
|
1102
|
+
const minParts = min.split('-');
|
|
1103
|
+
const minDate = new Date(parseInt(minParts[0], 10), parseInt(minParts[1], 10) - 1, parseInt(minParts[2], 10)).getTime();
|
|
1104
|
+
if (dateValue < minDate)
|
|
1105
|
+
return true;
|
|
1106
|
+
}
|
|
1107
|
+
if (max) {
|
|
1108
|
+
const maxParts = max.split('-');
|
|
1109
|
+
const maxDate = new Date(parseInt(maxParts[0], 10), parseInt(maxParts[1], 10) - 1, parseInt(maxParts[2], 10)).getTime();
|
|
1110
|
+
if (dateValue > maxDate)
|
|
1111
|
+
return true;
|
|
1112
|
+
}
|
|
1113
|
+
return false;
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Generates a complete DatePicker DOM descriptor tree.
|
|
1117
|
+
*
|
|
1118
|
+
* Produces a calendar grid with `role="grid"`, day cells with `role="gridcell"`,
|
|
1119
|
+
* month/year navigation buttons, weekday column headers with `role="columnheader"`,
|
|
1120
|
+
* and full accessibility attributes including `aria-label` on each day cell and
|
|
1121
|
+
* `aria-selected`/`aria-disabled` states.
|
|
1122
|
+
*/
|
|
1123
|
+
function generateDatePickerDOM(props) {
|
|
1124
|
+
const { value, min, max, locale = 'en-US', } = props;
|
|
1125
|
+
// Default displayMonth/displayYear to current date or value date
|
|
1126
|
+
const now = new Date();
|
|
1127
|
+
let displayMonth = props.displayMonth;
|
|
1128
|
+
let displayYear = props.displayYear;
|
|
1129
|
+
if (displayMonth === undefined || displayYear === undefined) {
|
|
1130
|
+
if (value) {
|
|
1131
|
+
const valueParts = value.split('-');
|
|
1132
|
+
displayMonth = displayMonth ?? (parseInt(valueParts[1], 10) - 1);
|
|
1133
|
+
displayYear = displayYear ?? parseInt(valueParts[0], 10);
|
|
1134
|
+
}
|
|
1135
|
+
else {
|
|
1136
|
+
displayMonth = displayMonth ?? now.getMonth();
|
|
1137
|
+
displayYear = displayYear ?? now.getFullYear();
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
// Generate unique IDs
|
|
1141
|
+
const gridId = generateId('ux4g-date-picker-grid');
|
|
1142
|
+
const headerId = generateId('ux4g-date-picker-header');
|
|
1143
|
+
// Build header with month/year display and navigation
|
|
1144
|
+
const monthName = getMonthName(displayYear, displayMonth, locale);
|
|
1145
|
+
const prevButton = {
|
|
1146
|
+
tag: 'button',
|
|
1147
|
+
props: {
|
|
1148
|
+
className: 'ux4g-date-picker-prev',
|
|
1149
|
+
type: 'button',
|
|
1150
|
+
'aria-label': 'Previous month',
|
|
1151
|
+
},
|
|
1152
|
+
children: [],
|
|
1153
|
+
};
|
|
1154
|
+
const nextButton = {
|
|
1155
|
+
tag: 'button',
|
|
1156
|
+
props: {
|
|
1157
|
+
className: 'ux4g-date-picker-next',
|
|
1158
|
+
type: 'button',
|
|
1159
|
+
'aria-label': 'Next month',
|
|
1160
|
+
},
|
|
1161
|
+
children: [],
|
|
1162
|
+
};
|
|
1163
|
+
const headerTitle = {
|
|
1164
|
+
tag: 'span',
|
|
1165
|
+
props: {
|
|
1166
|
+
className: 'ux4g-date-picker-title',
|
|
1167
|
+
},
|
|
1168
|
+
children: [`${monthName} ${displayYear}`],
|
|
1169
|
+
};
|
|
1170
|
+
const header = {
|
|
1171
|
+
tag: 'div',
|
|
1172
|
+
props: {
|
|
1173
|
+
className: 'ux4g-date-picker-header',
|
|
1174
|
+
id: headerId,
|
|
1175
|
+
},
|
|
1176
|
+
children: [prevButton, headerTitle, nextButton],
|
|
1177
|
+
};
|
|
1178
|
+
// Build weekday column headers
|
|
1179
|
+
const weekdays = getWeekdayHeaders(locale);
|
|
1180
|
+
const weekdayHeaders = weekdays.map((day) => ({
|
|
1181
|
+
tag: 'div',
|
|
1182
|
+
props: {
|
|
1183
|
+
className: 'ux4g-date-picker-weekday',
|
|
1184
|
+
role: 'columnheader',
|
|
1185
|
+
'aria-label': day,
|
|
1186
|
+
},
|
|
1187
|
+
children: [day],
|
|
1188
|
+
}));
|
|
1189
|
+
const weekdayRow = {
|
|
1190
|
+
tag: 'div',
|
|
1191
|
+
props: {
|
|
1192
|
+
className: 'ux4g-date-picker-weekdays',
|
|
1193
|
+
role: 'row',
|
|
1194
|
+
},
|
|
1195
|
+
children: weekdayHeaders,
|
|
1196
|
+
};
|
|
1197
|
+
// Build calendar grid with day cells
|
|
1198
|
+
const daysInMonth = getDaysInMonth(displayYear, displayMonth);
|
|
1199
|
+
const firstDayOfWeek = getFirstDayOfWeek(displayYear, displayMonth);
|
|
1200
|
+
// Parse selected value for comparison
|
|
1201
|
+
let selectedYear;
|
|
1202
|
+
let selectedMonth;
|
|
1203
|
+
let selectedDay;
|
|
1204
|
+
if (value) {
|
|
1205
|
+
const parts = value.split('-');
|
|
1206
|
+
selectedYear = parseInt(parts[0], 10);
|
|
1207
|
+
selectedMonth = parseInt(parts[1], 10) - 1;
|
|
1208
|
+
selectedDay = parseInt(parts[2], 10);
|
|
1209
|
+
}
|
|
1210
|
+
// Build week rows
|
|
1211
|
+
const weekRows = [];
|
|
1212
|
+
let currentDay = 1;
|
|
1213
|
+
let weekCells = [];
|
|
1214
|
+
// Fill empty cells before the first day
|
|
1215
|
+
for (let i = 0; i < firstDayOfWeek; i++) {
|
|
1216
|
+
weekCells.push({
|
|
1217
|
+
tag: 'div',
|
|
1218
|
+
props: {
|
|
1219
|
+
className: 'ux4g-date-picker-cell ux4g-date-picker-cell-empty',
|
|
1220
|
+
role: 'gridcell',
|
|
1221
|
+
},
|
|
1222
|
+
children: [],
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
// Fill days
|
|
1226
|
+
while (currentDay <= daysInMonth) {
|
|
1227
|
+
const isSelected = selectedYear === displayYear &&
|
|
1228
|
+
selectedMonth === displayMonth &&
|
|
1229
|
+
selectedDay === currentDay;
|
|
1230
|
+
const isDisabled = isDateDisabled(displayYear, displayMonth, currentDay, min, max);
|
|
1231
|
+
const dateLabel = formatDateLabel(displayYear, displayMonth, currentDay, locale);
|
|
1232
|
+
const cellProps = {
|
|
1233
|
+
className: 'ux4g-date-picker-cell',
|
|
1234
|
+
role: 'gridcell',
|
|
1235
|
+
'aria-label': dateLabel,
|
|
1236
|
+
};
|
|
1237
|
+
if (isSelected) {
|
|
1238
|
+
cellProps['aria-selected'] = 'true';
|
|
1239
|
+
}
|
|
1240
|
+
if (isDisabled) {
|
|
1241
|
+
cellProps['aria-disabled'] = 'true';
|
|
1242
|
+
}
|
|
1243
|
+
weekCells.push({
|
|
1244
|
+
tag: 'div',
|
|
1245
|
+
props: cellProps,
|
|
1246
|
+
children: [`${currentDay}`],
|
|
1247
|
+
});
|
|
1248
|
+
// If we've filled a week (7 cells), start a new row
|
|
1249
|
+
if (weekCells.length === 7) {
|
|
1250
|
+
weekRows.push({
|
|
1251
|
+
tag: 'div',
|
|
1252
|
+
props: {
|
|
1253
|
+
className: 'ux4g-date-picker-row',
|
|
1254
|
+
role: 'row',
|
|
1255
|
+
},
|
|
1256
|
+
children: weekCells,
|
|
1257
|
+
});
|
|
1258
|
+
weekCells = [];
|
|
1259
|
+
}
|
|
1260
|
+
currentDay++;
|
|
1261
|
+
}
|
|
1262
|
+
// Fill remaining empty cells in the last row
|
|
1263
|
+
if (weekCells.length > 0) {
|
|
1264
|
+
while (weekCells.length < 7) {
|
|
1265
|
+
weekCells.push({
|
|
1266
|
+
tag: 'div',
|
|
1267
|
+
props: {
|
|
1268
|
+
className: 'ux4g-date-picker-cell ux4g-date-picker-cell-empty',
|
|
1269
|
+
role: 'gridcell',
|
|
1270
|
+
},
|
|
1271
|
+
children: [],
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
weekRows.push({
|
|
1275
|
+
tag: 'div',
|
|
1276
|
+
props: {
|
|
1277
|
+
className: 'ux4g-date-picker-row',
|
|
1278
|
+
role: 'row',
|
|
1279
|
+
},
|
|
1280
|
+
children: weekCells,
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
// Build the grid
|
|
1284
|
+
const grid = {
|
|
1285
|
+
tag: 'div',
|
|
1286
|
+
props: {
|
|
1287
|
+
className: 'ux4g-date-picker-grid',
|
|
1288
|
+
id: gridId,
|
|
1289
|
+
role: 'grid',
|
|
1290
|
+
'aria-labelledby': headerId,
|
|
1291
|
+
},
|
|
1292
|
+
children: [weekdayRow, ...weekRows],
|
|
1293
|
+
};
|
|
1294
|
+
// Build root element
|
|
1295
|
+
const rootNode = {
|
|
1296
|
+
tag: 'div',
|
|
1297
|
+
props: {
|
|
1298
|
+
className: buildDateTimePickerClasses('date'),
|
|
1299
|
+
},
|
|
1300
|
+
children: [header, grid],
|
|
1301
|
+
};
|
|
1302
|
+
return rootNode;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// ux4g-components-web/src/dom-generators/stepper.ts
|
|
1306
|
+
// Framework-agnostic DOM Generator for the Stepper component.
|
|
1307
|
+
/**
|
|
1308
|
+
* Generates a complete Stepper DOM descriptor tree.
|
|
1309
|
+
*
|
|
1310
|
+
* Produces step indicators with completed/current/pending states.
|
|
1311
|
+
* Steps before `activeStep` are marked as completed, the step at
|
|
1312
|
+
* `activeStep` has `aria-current="step"`, and subsequent steps
|
|
1313
|
+
* are marked as pending.
|
|
1314
|
+
*/
|
|
1315
|
+
function generateStepperDOM(props) {
|
|
1316
|
+
const { steps, activeStep, orientation = 'horizontal', variant = 'default', size = 'default', } = props;
|
|
1317
|
+
// Validate steps is an array
|
|
1318
|
+
validateArray('Stepper', 'steps', steps);
|
|
1319
|
+
// Validate activeStep is in range [0, steps.length - 1]
|
|
1320
|
+
validateRange('Stepper', 'activeStep', activeStep, 0, steps.length - 1);
|
|
1321
|
+
// Build root className from buildStepperClasses
|
|
1322
|
+
const rootClassName = buildStepperClasses(orientation, undefined, variant, size);
|
|
1323
|
+
// Generate step indicator elements
|
|
1324
|
+
const stepChildren = steps.map((step, index) => {
|
|
1325
|
+
let stepClassName;
|
|
1326
|
+
const stepProps = {};
|
|
1327
|
+
if (index < activeStep) {
|
|
1328
|
+
// Completed step
|
|
1329
|
+
stepClassName = 'ux4g-stepper-item completed';
|
|
1330
|
+
stepProps['aria-label'] = `Completed: ${step.label}`;
|
|
1331
|
+
}
|
|
1332
|
+
else if (index === activeStep) {
|
|
1333
|
+
// Current step
|
|
1334
|
+
stepClassName = 'ux4g-stepper-item current';
|
|
1335
|
+
stepProps['aria-current'] = 'step';
|
|
1336
|
+
stepProps['aria-label'] = step.label;
|
|
1337
|
+
}
|
|
1338
|
+
else {
|
|
1339
|
+
// Pending step
|
|
1340
|
+
stepClassName = 'ux4g-stepper-item pending';
|
|
1341
|
+
stepProps['aria-label'] = step.label;
|
|
1342
|
+
}
|
|
1343
|
+
stepProps.className = stepClassName;
|
|
1344
|
+
// Build children for the step indicator
|
|
1345
|
+
const stepItemChildren = [
|
|
1346
|
+
{
|
|
1347
|
+
tag: 'span',
|
|
1348
|
+
props: {
|
|
1349
|
+
className: 'ux4g-stepper-indicator',
|
|
1350
|
+
},
|
|
1351
|
+
children: [`${index + 1}`],
|
|
1352
|
+
},
|
|
1353
|
+
{
|
|
1354
|
+
tag: 'span',
|
|
1355
|
+
props: {
|
|
1356
|
+
className: 'ux4g-stepper-label',
|
|
1357
|
+
},
|
|
1358
|
+
children: step.description
|
|
1359
|
+
? [
|
|
1360
|
+
step.label,
|
|
1361
|
+
{
|
|
1362
|
+
tag: 'span',
|
|
1363
|
+
props: {
|
|
1364
|
+
className: 'ux4g-stepper-description',
|
|
1365
|
+
},
|
|
1366
|
+
children: [step.description],
|
|
1367
|
+
},
|
|
1368
|
+
]
|
|
1369
|
+
: [step.label],
|
|
1370
|
+
},
|
|
1371
|
+
];
|
|
1372
|
+
return {
|
|
1373
|
+
tag: 'div',
|
|
1374
|
+
props: stepProps,
|
|
1375
|
+
children: stepItemChildren,
|
|
1376
|
+
};
|
|
1377
|
+
});
|
|
1378
|
+
// Build root element
|
|
1379
|
+
const rootNode = {
|
|
1380
|
+
tag: 'div',
|
|
1381
|
+
props: {
|
|
1382
|
+
className: rootClassName,
|
|
1383
|
+
role: 'list',
|
|
1384
|
+
'aria-label': 'Progress',
|
|
1385
|
+
},
|
|
1386
|
+
children: stepChildren,
|
|
1387
|
+
};
|
|
1388
|
+
return rootNode;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// ux4g-components-web/src/dom-generators/file-upload.ts
|
|
1392
|
+
// Framework-agnostic DOM Generator for the FileUpload component.
|
|
1393
|
+
/**
|
|
1394
|
+
* Formats a byte value into a human-readable string (e.g., "1.5 KB", "2.3 MB").
|
|
1395
|
+
*/
|
|
1396
|
+
function formatFileSize(bytes) {
|
|
1397
|
+
if (bytes < 1024)
|
|
1398
|
+
return `${bytes} B`;
|
|
1399
|
+
if (bytes < 1024 * 1024)
|
|
1400
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1401
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Generates a complete FileUpload DOM descriptor tree.
|
|
1405
|
+
*
|
|
1406
|
+
* Produces a drop zone with `role="button"`, a visually hidden file input,
|
|
1407
|
+
* and a file list displaying name and size of each selected file.
|
|
1408
|
+
* Compatible with Runtime_JS MutationObserver-based initialization.
|
|
1409
|
+
*/
|
|
1410
|
+
function generateFileUploadDOM(props) {
|
|
1411
|
+
const { files = [], accept, maxSizeBytes, multiple = false, state, } = props;
|
|
1412
|
+
// Generate unique IDs
|
|
1413
|
+
const inputId = generateId('ux4g-file-upload-input');
|
|
1414
|
+
const dropZoneId = generateId('ux4g-file-upload-zone');
|
|
1415
|
+
// Build CSS classes via Class Builder
|
|
1416
|
+
const rootClasses = buildFileUploadClasses(state);
|
|
1417
|
+
// Build hidden file input
|
|
1418
|
+
const inputProps = {
|
|
1419
|
+
className: 'ux4g-upload-input',
|
|
1420
|
+
id: inputId,
|
|
1421
|
+
type: 'file',
|
|
1422
|
+
'aria-hidden': 'true',
|
|
1423
|
+
tabIndex: -1,
|
|
1424
|
+
};
|
|
1425
|
+
if (accept) {
|
|
1426
|
+
inputProps['accept'] = accept;
|
|
1427
|
+
}
|
|
1428
|
+
if (multiple) {
|
|
1429
|
+
inputProps['multiple'] = true;
|
|
1430
|
+
}
|
|
1431
|
+
const inputNode = {
|
|
1432
|
+
tag: 'input',
|
|
1433
|
+
props: inputProps,
|
|
1434
|
+
children: [],
|
|
1435
|
+
};
|
|
1436
|
+
// Build drop zone
|
|
1437
|
+
const dropZoneNode = {
|
|
1438
|
+
tag: 'div',
|
|
1439
|
+
props: {
|
|
1440
|
+
className: 'ux4g-upload-drop-zone',
|
|
1441
|
+
id: dropZoneId,
|
|
1442
|
+
role: 'button',
|
|
1443
|
+
tabIndex: 0,
|
|
1444
|
+
'aria-label': 'Drop files here or click to upload',
|
|
1445
|
+
'aria-controls': inputId,
|
|
1446
|
+
},
|
|
1447
|
+
children: [
|
|
1448
|
+
{
|
|
1449
|
+
tag: 'span',
|
|
1450
|
+
props: {
|
|
1451
|
+
className: 'ux4g-upload-drop-zone-text',
|
|
1452
|
+
},
|
|
1453
|
+
children: ['Drop files here or click to upload'],
|
|
1454
|
+
},
|
|
1455
|
+
],
|
|
1456
|
+
};
|
|
1457
|
+
// Build file list items
|
|
1458
|
+
const fileItems = files.map((file) => ({
|
|
1459
|
+
tag: 'div',
|
|
1460
|
+
props: {
|
|
1461
|
+
className: 'ux4g-upload-file-item',
|
|
1462
|
+
},
|
|
1463
|
+
children: [
|
|
1464
|
+
{
|
|
1465
|
+
tag: 'span',
|
|
1466
|
+
props: {
|
|
1467
|
+
className: 'ux4g-upload-file-name',
|
|
1468
|
+
},
|
|
1469
|
+
children: [file.name],
|
|
1470
|
+
},
|
|
1471
|
+
{
|
|
1472
|
+
tag: 'span',
|
|
1473
|
+
props: {
|
|
1474
|
+
className: 'ux4g-upload-file-size',
|
|
1475
|
+
},
|
|
1476
|
+
children: [formatFileSize(file.size)],
|
|
1477
|
+
},
|
|
1478
|
+
],
|
|
1479
|
+
}));
|
|
1480
|
+
// Build file list container
|
|
1481
|
+
const fileListNode = {
|
|
1482
|
+
tag: 'div',
|
|
1483
|
+
props: {
|
|
1484
|
+
className: 'ux4g-upload-file-list',
|
|
1485
|
+
role: 'list',
|
|
1486
|
+
'aria-label': 'Uploaded files',
|
|
1487
|
+
},
|
|
1488
|
+
children: fileItems,
|
|
1489
|
+
};
|
|
1490
|
+
// Build root container
|
|
1491
|
+
const rootNode = {
|
|
1492
|
+
tag: 'div',
|
|
1493
|
+
props: {
|
|
1494
|
+
className: rootClasses,
|
|
1495
|
+
},
|
|
1496
|
+
children: [dropZoneNode, inputNode, fileListNode],
|
|
1497
|
+
};
|
|
1498
|
+
return rootNode;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// ux4g-components-web/src/dom-generators/progress.ts
|
|
1502
|
+
// Framework-agnostic DOM Generator for the Progress Indicator component.
|
|
1503
|
+
const VALID_TYPES$1 = ['bar', 'circle'];
|
|
1504
|
+
const VALID_SIZES$2 = ['xs', 's', 'm', 'l', 'xl', '2xl', '3xl'];
|
|
1505
|
+
/**
|
|
1506
|
+
* Generates a complete Progress Indicator DOM descriptor tree.
|
|
1507
|
+
*
|
|
1508
|
+
* Produces a progress element with `role="progressbar"`,
|
|
1509
|
+
* `aria-valuenow`, `aria-valuemin`, and `aria-valuemax`.
|
|
1510
|
+
* Supports both 'bar' and 'circle' visualization types.
|
|
1511
|
+
*/
|
|
1512
|
+
function generateProgressDOM(props) {
|
|
1513
|
+
const { value, type = 'bar', size, label, } = props;
|
|
1514
|
+
// Validate props
|
|
1515
|
+
validateRange('Progress', 'value', value, 0, 100);
|
|
1516
|
+
if (type !== undefined) {
|
|
1517
|
+
validateEnum('Progress', 'type', type, VALID_TYPES$1);
|
|
1518
|
+
}
|
|
1519
|
+
if (size !== undefined) {
|
|
1520
|
+
validateEnum('Progress', 'size', size, VALID_SIZES$2);
|
|
1521
|
+
}
|
|
1522
|
+
const ariaLabel = label || 'Progress';
|
|
1523
|
+
// Build CSS class string from the class builder
|
|
1524
|
+
const className = buildProgressIndicatorClasses(type);
|
|
1525
|
+
if (type === 'circle') {
|
|
1526
|
+
return generateCircleProgress(value, className, size, ariaLabel);
|
|
1527
|
+
}
|
|
1528
|
+
return generateBarProgress(value, className, size, ariaLabel);
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Generates a bar-type progress indicator.
|
|
1532
|
+
*/
|
|
1533
|
+
function generateBarProgress(value, className, size, ariaLabel) {
|
|
1534
|
+
// The fill element represents the completed portion
|
|
1535
|
+
const fillNode = {
|
|
1536
|
+
tag: 'div',
|
|
1537
|
+
props: {
|
|
1538
|
+
className: 'ux4g-progress-bar-fill',
|
|
1539
|
+
'data-value': String(value),
|
|
1540
|
+
},
|
|
1541
|
+
children: [],
|
|
1542
|
+
};
|
|
1543
|
+
// The track element contains the fill
|
|
1544
|
+
const trackNode = {
|
|
1545
|
+
tag: 'div',
|
|
1546
|
+
props: {
|
|
1547
|
+
className: 'ux4g-progress-bar-track',
|
|
1548
|
+
},
|
|
1549
|
+
children: [fillNode],
|
|
1550
|
+
};
|
|
1551
|
+
// Root element with ARIA progressbar role
|
|
1552
|
+
const rootProps = {
|
|
1553
|
+
className,
|
|
1554
|
+
role: 'progressbar',
|
|
1555
|
+
'aria-valuenow': value,
|
|
1556
|
+
'aria-valuemin': 0,
|
|
1557
|
+
'aria-valuemax': 100,
|
|
1558
|
+
'aria-label': ariaLabel,
|
|
1559
|
+
};
|
|
1560
|
+
if (size) {
|
|
1561
|
+
rootProps['data-ux-size'] = size;
|
|
1562
|
+
}
|
|
1563
|
+
const rootNode = {
|
|
1564
|
+
tag: 'div',
|
|
1565
|
+
props: rootProps,
|
|
1566
|
+
children: [trackNode],
|
|
1567
|
+
};
|
|
1568
|
+
return rootNode;
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Generates a circle-type progress indicator.
|
|
1572
|
+
*/
|
|
1573
|
+
function generateCircleProgress(value, className, size, ariaLabel) {
|
|
1574
|
+
// The circle container element
|
|
1575
|
+
const circleContainer = {
|
|
1576
|
+
tag: 'div',
|
|
1577
|
+
props: {
|
|
1578
|
+
className: 'ux4g-progress-circle-container',
|
|
1579
|
+
'data-value': String(value),
|
|
1580
|
+
},
|
|
1581
|
+
children: [],
|
|
1582
|
+
};
|
|
1583
|
+
// Root element with ARIA progressbar role
|
|
1584
|
+
const rootProps = {
|
|
1585
|
+
className,
|
|
1586
|
+
role: 'progressbar',
|
|
1587
|
+
'aria-valuenow': value,
|
|
1588
|
+
'aria-valuemin': 0,
|
|
1589
|
+
'aria-valuemax': 100,
|
|
1590
|
+
'aria-label': ariaLabel,
|
|
1591
|
+
};
|
|
1592
|
+
if (size) {
|
|
1593
|
+
rootProps['data-ux-size'] = size;
|
|
1594
|
+
}
|
|
1595
|
+
const rootNode = {
|
|
1596
|
+
tag: 'div',
|
|
1597
|
+
props: rootProps,
|
|
1598
|
+
children: [circleContainer],
|
|
1599
|
+
};
|
|
1600
|
+
return rootNode;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
// ux4g-components-web/src/dom-generators/table.ts
|
|
1604
|
+
// Framework-agnostic DOM Generator for the Table component.
|
|
1605
|
+
/**
|
|
1606
|
+
* Generates a complete Table DOM descriptor tree.
|
|
1607
|
+
*
|
|
1608
|
+
* Produces a `<table>` with `<thead>` containing one `<th>` per column
|
|
1609
|
+
* and `<tbody>` containing one `<tr>` per data row, each with one `<td>` per column.
|
|
1610
|
+
* Sortable columns receive `aria-sort="none"` on their `<th>`.
|
|
1611
|
+
*/
|
|
1612
|
+
function generateTableDOM(props) {
|
|
1613
|
+
const { columns, data, size = 'm', divider, zebra, interactive = false, } = props;
|
|
1614
|
+
// Validate required props
|
|
1615
|
+
validateArray('Table', 'columns', columns);
|
|
1616
|
+
// Determine if any column is sortable (for buildTableClasses sortable param)
|
|
1617
|
+
const hasSortable = columns.some((col) => col.sortable);
|
|
1618
|
+
// Build CSS classes via Class Builder
|
|
1619
|
+
const tableClasses = buildTableClasses(size, divider, zebra, interactive, hasSortable);
|
|
1620
|
+
// Build thead with th elements
|
|
1621
|
+
const thElements = columns.map((col) => {
|
|
1622
|
+
const thProps = {
|
|
1623
|
+
scope: 'col',
|
|
1624
|
+
};
|
|
1625
|
+
if (col.sortable) {
|
|
1626
|
+
thProps['aria-sort'] = 'none';
|
|
1627
|
+
}
|
|
1628
|
+
if (col.width) {
|
|
1629
|
+
thProps['style'] = `width: ${col.width}`;
|
|
1630
|
+
}
|
|
1631
|
+
return {
|
|
1632
|
+
tag: 'th',
|
|
1633
|
+
props: thProps,
|
|
1634
|
+
children: [col.label],
|
|
1635
|
+
};
|
|
1636
|
+
});
|
|
1637
|
+
const theadRow = {
|
|
1638
|
+
tag: 'tr',
|
|
1639
|
+
props: {},
|
|
1640
|
+
children: thElements,
|
|
1641
|
+
};
|
|
1642
|
+
const thead = {
|
|
1643
|
+
tag: 'thead',
|
|
1644
|
+
props: {},
|
|
1645
|
+
children: [theadRow],
|
|
1646
|
+
};
|
|
1647
|
+
// Build tbody with tr/td elements
|
|
1648
|
+
const bodyRows = (data || []).map((row) => {
|
|
1649
|
+
const cells = columns.map((col) => {
|
|
1650
|
+
const cellValue = row[col.key];
|
|
1651
|
+
const cellText = cellValue != null ? String(cellValue) : '';
|
|
1652
|
+
return {
|
|
1653
|
+
tag: 'td',
|
|
1654
|
+
props: {},
|
|
1655
|
+
children: [cellText],
|
|
1656
|
+
};
|
|
1657
|
+
});
|
|
1658
|
+
return {
|
|
1659
|
+
tag: 'tr',
|
|
1660
|
+
props: {},
|
|
1661
|
+
children: cells,
|
|
1662
|
+
};
|
|
1663
|
+
});
|
|
1664
|
+
const tbody = {
|
|
1665
|
+
tag: 'tbody',
|
|
1666
|
+
props: {},
|
|
1667
|
+
children: bodyRows,
|
|
1668
|
+
};
|
|
1669
|
+
// Build root table element
|
|
1670
|
+
const rootNode = {
|
|
1671
|
+
tag: 'table',
|
|
1672
|
+
props: {
|
|
1673
|
+
className: tableClasses,
|
|
1674
|
+
},
|
|
1675
|
+
children: [thead, tbody],
|
|
1676
|
+
};
|
|
1677
|
+
return rootNode;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// ux4g-components-web/src/dom-generators/otp.ts
|
|
1681
|
+
// Framework-agnostic DOM Generator for the OTP Input component.
|
|
1682
|
+
/**
|
|
1683
|
+
* Generates a complete OTP Input DOM descriptor tree.
|
|
1684
|
+
*
|
|
1685
|
+
* Produces individual `<input>` cells each accepting a single character,
|
|
1686
|
+
* with maxLength=1. The root container gets CSS classes from `buildOtpInputClasses(state)`.
|
|
1687
|
+
* When disabled, each cell receives `aria-disabled="true"` and `disabled=true`.
|
|
1688
|
+
*/
|
|
1689
|
+
function generateOtpDOM(props) {
|
|
1690
|
+
const { length, values = [], state, disabled = false, } = props;
|
|
1691
|
+
// Validate length is in range [4, 8]
|
|
1692
|
+
validateRange('OTP', 'length', length, 4, 8);
|
|
1693
|
+
// Build CSS classes via Class Builder
|
|
1694
|
+
const containerClasses = buildOtpInputClasses(state);
|
|
1695
|
+
// Generate individual input cells
|
|
1696
|
+
const cells = [];
|
|
1697
|
+
for (let i = 0; i < length; i++) {
|
|
1698
|
+
const cellId = generateId('ux4g-otp-cell');
|
|
1699
|
+
const cellProps = {
|
|
1700
|
+
className: 'ux4g-otp-cell',
|
|
1701
|
+
id: cellId,
|
|
1702
|
+
type: 'text',
|
|
1703
|
+
maxLength: '1',
|
|
1704
|
+
'aria-label': `Digit ${i + 1}`,
|
|
1705
|
+
};
|
|
1706
|
+
// Set value if provided
|
|
1707
|
+
if (values[i] !== undefined) {
|
|
1708
|
+
cellProps['value'] = values[i];
|
|
1709
|
+
}
|
|
1710
|
+
// Apply disabled state
|
|
1711
|
+
if (disabled) {
|
|
1712
|
+
cellProps['aria-disabled'] = 'true';
|
|
1713
|
+
cellProps['disabled'] = true;
|
|
1714
|
+
}
|
|
1715
|
+
cells.push({
|
|
1716
|
+
tag: 'input',
|
|
1717
|
+
props: cellProps,
|
|
1718
|
+
children: [],
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
// Build root container
|
|
1722
|
+
const rootNode = {
|
|
1723
|
+
tag: 'div',
|
|
1724
|
+
props: {
|
|
1725
|
+
className: containerClasses,
|
|
1726
|
+
},
|
|
1727
|
+
children: cells,
|
|
1728
|
+
};
|
|
1729
|
+
return rootNode;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
// ux4g-components-web/src/dom-generators/avatar.ts
|
|
1733
|
+
// Framework-agnostic DOM Generator for the Avatar (native) component.
|
|
1734
|
+
const VALID_SIZES$1 = ['xs', 's', 'm', 'l', 'xl', '2xl', '3xl'];
|
|
1735
|
+
const VALID_STATUSES = ['online', 'offline', 'busy', 'away'];
|
|
1736
|
+
/**
|
|
1737
|
+
* Generates a complete Avatar DOM descriptor tree.
|
|
1738
|
+
*
|
|
1739
|
+
* Produces a container with avatar classes from buildAvatarClasses(),
|
|
1740
|
+
* an img element with alt text (or fallback initials span),
|
|
1741
|
+
* and an optional status indicator element.
|
|
1742
|
+
*/
|
|
1743
|
+
function generateAvatarDOM(props) {
|
|
1744
|
+
const { src, alt = '', size, status, fallbackInitials, } = props;
|
|
1745
|
+
// Validate props
|
|
1746
|
+
if (size !== undefined) {
|
|
1747
|
+
validateEnum('Avatar', 'size', size, VALID_SIZES$1);
|
|
1748
|
+
}
|
|
1749
|
+
if (status !== undefined) {
|
|
1750
|
+
validateEnum('Avatar', 'status', status, VALID_STATUSES);
|
|
1751
|
+
}
|
|
1752
|
+
// Use 'status' avatar type when a status prop is provided, otherwise 'profile'
|
|
1753
|
+
const avatarType = status ? 'status' : 'profile';
|
|
1754
|
+
// Build CSS class string from the class builder
|
|
1755
|
+
const className = buildAvatarClasses(avatarType, size);
|
|
1756
|
+
// Build children
|
|
1757
|
+
const children = [];
|
|
1758
|
+
// Add img element or fallback initials
|
|
1759
|
+
if (src) {
|
|
1760
|
+
const imgNode = {
|
|
1761
|
+
tag: 'img',
|
|
1762
|
+
props: {
|
|
1763
|
+
className: 'ux4g-avatar-img',
|
|
1764
|
+
src,
|
|
1765
|
+
alt,
|
|
1766
|
+
},
|
|
1767
|
+
children: [],
|
|
1768
|
+
};
|
|
1769
|
+
children.push(imgNode);
|
|
1770
|
+
}
|
|
1771
|
+
else if (fallbackInitials) {
|
|
1772
|
+
const initialsNode = {
|
|
1773
|
+
tag: 'span',
|
|
1774
|
+
props: {
|
|
1775
|
+
className: 'ux4g-avatar-initials',
|
|
1776
|
+
'aria-label': alt || fallbackInitials,
|
|
1777
|
+
},
|
|
1778
|
+
children: [fallbackInitials],
|
|
1779
|
+
};
|
|
1780
|
+
children.push(initialsNode);
|
|
1781
|
+
}
|
|
1782
|
+
// Add optional status indicator
|
|
1783
|
+
if (status) {
|
|
1784
|
+
const statusNode = {
|
|
1785
|
+
tag: 'span',
|
|
1786
|
+
props: {
|
|
1787
|
+
className: `ux4g-avatar-status-indicator ux4g-avatar-status-${status}`,
|
|
1788
|
+
'aria-label': status,
|
|
1789
|
+
},
|
|
1790
|
+
children: [],
|
|
1791
|
+
};
|
|
1792
|
+
children.push(statusNode);
|
|
1793
|
+
}
|
|
1794
|
+
// Root container element
|
|
1795
|
+
const rootNode = {
|
|
1796
|
+
tag: 'div',
|
|
1797
|
+
props: {
|
|
1798
|
+
className,
|
|
1799
|
+
role: 'img',
|
|
1800
|
+
'aria-label': alt || fallbackInitials || 'Avatar',
|
|
1801
|
+
},
|
|
1802
|
+
children,
|
|
1803
|
+
};
|
|
1804
|
+
return rootNode;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// ux4g-components-web/src/dom-generators/tooltip.ts
|
|
1808
|
+
// Framework-agnostic DOM Generator for the Tooltip component.
|
|
1809
|
+
/**
|
|
1810
|
+
* Generates a complete Tooltip DOM descriptor tree.
|
|
1811
|
+
*
|
|
1812
|
+
* Produces a tooltip container with `role="tooltip"`, an arrow element,
|
|
1813
|
+
* `data-ux4g-tooltip` on the trigger element, `data-placement` for positioning,
|
|
1814
|
+
* and `aria-describedby` on the trigger referencing the tooltip ID.
|
|
1815
|
+
* Compatible with Runtime_JS MutationObserver-based initialization.
|
|
1816
|
+
*/
|
|
1817
|
+
function generateTooltipDOM(props) {
|
|
1818
|
+
const { text, placement = 'top-center', size = 's', triggerId, } = props;
|
|
1819
|
+
// Validate required props
|
|
1820
|
+
validateRequired('Tooltip', 'text', text);
|
|
1821
|
+
validateType('Tooltip', 'text', text, 'string');
|
|
1822
|
+
// Generate unique IDs
|
|
1823
|
+
const tooltipId = generateId('ux4g-tooltip');
|
|
1824
|
+
const triggerElId = triggerId || generateId('ux4g-tooltip-trigger');
|
|
1825
|
+
// Build CSS classes via Class Builder
|
|
1826
|
+
const tooltipClassName = buildTooltipClasses(placement, size);
|
|
1827
|
+
// Map placement to the simplified data-placement value expected by Runtime_JS
|
|
1828
|
+
// Runtime_JS expects: top, bottom, left, right
|
|
1829
|
+
let dataPlacement;
|
|
1830
|
+
if (placement.startsWith('top')) {
|
|
1831
|
+
dataPlacement = 'top';
|
|
1832
|
+
}
|
|
1833
|
+
else if (placement.startsWith('bottom')) {
|
|
1834
|
+
dataPlacement = 'bottom';
|
|
1835
|
+
}
|
|
1836
|
+
else if (placement.startsWith('left')) {
|
|
1837
|
+
dataPlacement = 'left';
|
|
1838
|
+
}
|
|
1839
|
+
else {
|
|
1840
|
+
dataPlacement = 'right';
|
|
1841
|
+
}
|
|
1842
|
+
// Build the arrow element
|
|
1843
|
+
const arrowNode = {
|
|
1844
|
+
tag: 'span',
|
|
1845
|
+
props: {
|
|
1846
|
+
className: 'ux4g-tooltip-arrow',
|
|
1847
|
+
},
|
|
1848
|
+
children: [],
|
|
1849
|
+
};
|
|
1850
|
+
// Build the tooltip content element
|
|
1851
|
+
const tooltipNode = {
|
|
1852
|
+
tag: 'div',
|
|
1853
|
+
props: {
|
|
1854
|
+
className: tooltipClassName,
|
|
1855
|
+
id: tooltipId,
|
|
1856
|
+
role: 'tooltip',
|
|
1857
|
+
'data-placement': dataPlacement,
|
|
1858
|
+
},
|
|
1859
|
+
children: [arrowNode, text],
|
|
1860
|
+
};
|
|
1861
|
+
// Build the trigger element with data-ux4g-tooltip and aria-describedby
|
|
1862
|
+
const triggerNode = {
|
|
1863
|
+
tag: 'span',
|
|
1864
|
+
props: {
|
|
1865
|
+
className: 'ux4g-tooltip-trigger',
|
|
1866
|
+
id: triggerElId,
|
|
1867
|
+
'data-ux4g-tooltip': 'true',
|
|
1868
|
+
'aria-describedby': tooltipId,
|
|
1869
|
+
tabIndex: 0,
|
|
1870
|
+
},
|
|
1871
|
+
children: [],
|
|
1872
|
+
};
|
|
1873
|
+
// Build root wrapper containing trigger and tooltip
|
|
1874
|
+
const rootNode = {
|
|
1875
|
+
tag: 'div',
|
|
1876
|
+
props: {
|
|
1877
|
+
className: 'ux4g-tooltip-wrapper',
|
|
1878
|
+
},
|
|
1879
|
+
children: [triggerNode, tooltipNode],
|
|
1880
|
+
};
|
|
1881
|
+
return rootNode;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
// ux4g-components-web/src/dom-generators/chip.ts
|
|
1885
|
+
// Framework-agnostic DOM Generator for the Chip (native) component.
|
|
1886
|
+
const VALID_TYPES = ['filter', 'choice', 'input'];
|
|
1887
|
+
const VALID_SIZES = ['md', 'sm', 'xs'];
|
|
1888
|
+
/**
|
|
1889
|
+
* Generates a complete Chip DOM descriptor tree.
|
|
1890
|
+
*
|
|
1891
|
+
* Produces a chip container with CSS classes from `buildChipClasses()`,
|
|
1892
|
+
* a label span, and an optional dismiss button with `aria-label="Remove {label}"`.
|
|
1893
|
+
*/
|
|
1894
|
+
function generateChipDOM(props) {
|
|
1895
|
+
const { label, type = 'filter', size = 'md', active = false, dismissible = false, } = props;
|
|
1896
|
+
// Validate required props
|
|
1897
|
+
validateRequired('Chip', 'label', label);
|
|
1898
|
+
validateType('Chip', 'label', label, 'string');
|
|
1899
|
+
if (type !== undefined) {
|
|
1900
|
+
validateEnum('Chip', 'type', type, VALID_TYPES);
|
|
1901
|
+
}
|
|
1902
|
+
if (size !== undefined) {
|
|
1903
|
+
validateEnum('Chip', 'size', size, VALID_SIZES);
|
|
1904
|
+
}
|
|
1905
|
+
// Build CSS class string from the class builder
|
|
1906
|
+
const className = buildChipClasses(type, size, active);
|
|
1907
|
+
// Label span
|
|
1908
|
+
const labelNode = {
|
|
1909
|
+
tag: 'span',
|
|
1910
|
+
props: {
|
|
1911
|
+
className: 'ux4g-chip-label',
|
|
1912
|
+
},
|
|
1913
|
+
children: [label],
|
|
1914
|
+
};
|
|
1915
|
+
// Build children array
|
|
1916
|
+
const children = [labelNode];
|
|
1917
|
+
// Conditional dismiss button
|
|
1918
|
+
if (dismissible) {
|
|
1919
|
+
const dismissButton = {
|
|
1920
|
+
tag: 'button',
|
|
1921
|
+
props: {
|
|
1922
|
+
className: 'ux4g-chip-dismiss',
|
|
1923
|
+
type: 'button',
|
|
1924
|
+
'aria-label': `Remove ${label}`,
|
|
1925
|
+
},
|
|
1926
|
+
children: [],
|
|
1927
|
+
};
|
|
1928
|
+
children.push(dismissButton);
|
|
1929
|
+
}
|
|
1930
|
+
// Root container
|
|
1931
|
+
const rootNode = {
|
|
1932
|
+
tag: 'div',
|
|
1933
|
+
props: {
|
|
1934
|
+
className,
|
|
1935
|
+
role: 'option',
|
|
1936
|
+
},
|
|
1937
|
+
children,
|
|
1938
|
+
};
|
|
1939
|
+
return rootNode;
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
// ux4g-components-web/src/dom-generators/popover.ts
|
|
1943
|
+
// Framework-agnostic DOM Generator for the Popover component.
|
|
1944
|
+
const VALID_PLACEMENTS = [
|
|
1945
|
+
'top', 'top-start', 'top-end',
|
|
1946
|
+
'bottom', 'bottom-start', 'bottom-end',
|
|
1947
|
+
'left', 'left-start', 'left-end',
|
|
1948
|
+
'right', 'right-start', 'right-end',
|
|
1949
|
+
];
|
|
1950
|
+
/**
|
|
1951
|
+
* Generates a complete Popover DOM descriptor tree.
|
|
1952
|
+
*
|
|
1953
|
+
* Produces a trigger wrapper element with `data-ux4g-popover` and
|
|
1954
|
+
* `aria-describedby` referencing the popover content container.
|
|
1955
|
+
* The popover container uses CSS classes from `buildPopoverClasses()`
|
|
1956
|
+
* for placement and visibility. Compatible with Runtime_JS
|
|
1957
|
+
* MutationObserver-based initialization.
|
|
1958
|
+
*/
|
|
1959
|
+
function generatePopoverDOM(props) {
|
|
1960
|
+
const { placement = 'bottom', show = false, triggerId, } = props;
|
|
1961
|
+
// Validate placement prop if provided explicitly
|
|
1962
|
+
if (props.placement !== undefined) {
|
|
1963
|
+
validateEnum('Popover', 'placement', placement, VALID_PLACEMENTS);
|
|
1964
|
+
}
|
|
1965
|
+
// Generate unique IDs
|
|
1966
|
+
const resolvedTriggerId = triggerId ?? generateId('ux4g-popover-trigger');
|
|
1967
|
+
const popoverId = generateId('ux4g-popover-content');
|
|
1968
|
+
// Build CSS classes via Class Builder
|
|
1969
|
+
const popoverClasses = buildPopoverClasses(placement, show);
|
|
1970
|
+
// Build trigger element
|
|
1971
|
+
const triggerNode = {
|
|
1972
|
+
tag: 'span',
|
|
1973
|
+
props: {
|
|
1974
|
+
className: 'ux4g-popover-trigger',
|
|
1975
|
+
id: resolvedTriggerId,
|
|
1976
|
+
'data-ux4g-popover': 'true',
|
|
1977
|
+
'aria-describedby': popoverId,
|
|
1978
|
+
},
|
|
1979
|
+
children: [],
|
|
1980
|
+
};
|
|
1981
|
+
// Build popover content container
|
|
1982
|
+
const popoverContentNode = {
|
|
1983
|
+
tag: 'div',
|
|
1984
|
+
props: {
|
|
1985
|
+
className: popoverClasses,
|
|
1986
|
+
id: popoverId,
|
|
1987
|
+
role: 'tooltip',
|
|
1988
|
+
'data-placement': placement,
|
|
1989
|
+
},
|
|
1990
|
+
children: [],
|
|
1991
|
+
};
|
|
1992
|
+
// Build root wrapper containing trigger + popover content
|
|
1993
|
+
const rootNode = {
|
|
1994
|
+
tag: 'div',
|
|
1995
|
+
props: {
|
|
1996
|
+
className: 'ux4g-popover-wrapper',
|
|
1997
|
+
},
|
|
1998
|
+
children: [triggerNode, popoverContentNode],
|
|
1999
|
+
};
|
|
2000
|
+
return rootNode;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
exports.DOMGeneratorValidationError = DOMGeneratorValidationError;
|
|
2004
|
+
exports.generateAccordionDOM = generateAccordionDOM;
|
|
2005
|
+
exports.generateAvatarDOM = generateAvatarDOM;
|
|
2006
|
+
exports.generateCarouselDOM = generateCarouselDOM;
|
|
2007
|
+
exports.generateChipDOM = generateChipDOM;
|
|
2008
|
+
exports.generateComboboxDOM = generateComboboxDOM;
|
|
2009
|
+
exports.generateDatePickerDOM = generateDatePickerDOM;
|
|
2010
|
+
exports.generateDrawerDOM = generateDrawerDOM;
|
|
2011
|
+
exports.generateDropdownDOM = generateDropdownDOM;
|
|
2012
|
+
exports.generateFileUploadDOM = generateFileUploadDOM;
|
|
2013
|
+
exports.generateId = generateId;
|
|
2014
|
+
exports.generateModalDOM = generateModalDOM;
|
|
2015
|
+
exports.generateOtpDOM = generateOtpDOM;
|
|
2016
|
+
exports.generatePopoverDOM = generatePopoverDOM;
|
|
2017
|
+
exports.generateProgressDOM = generateProgressDOM;
|
|
2018
|
+
exports.generateSearchDOM = generateSearchDOM;
|
|
2019
|
+
exports.generateStepperDOM = generateStepperDOM;
|
|
2020
|
+
exports.generateTableDOM = generateTableDOM;
|
|
2021
|
+
exports.generateTabsDOM = generateTabsDOM;
|
|
2022
|
+
exports.generateTimePickerDOM = generateTimePickerDOM;
|
|
2023
|
+
exports.generateTooltipDOM = generateTooltipDOM;
|
|
2024
|
+
exports.resetIdCounter = resetIdCounter;
|
|
2025
|
+
exports.validateArray = validateArray;
|
|
2026
|
+
exports.validateEnum = validateEnum;
|
|
2027
|
+
exports.validateRange = validateRange;
|
|
2028
|
+
exports.validateRequired = validateRequired;
|
|
2029
|
+
exports.validateType = validateType;
|