pulse-js-framework 1.7.9 → 1.7.10

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.
@@ -8,6 +8,9 @@
8
8
  import { effect, onCleanup } from './pulse.js';
9
9
  import { sanitizeUrl, safeSetStyle } from './utils.js';
10
10
  import { getAdapter } from './dom-adapter.js';
11
+ import { loggers } from './logger.js';
12
+
13
+ const log = loggers.dom;
11
14
 
12
15
  // =============================================================================
13
16
  // URL ATTRIBUTES (XSS Protection)
@@ -53,8 +56,8 @@ export function bind(element, attr, getValue) {
53
56
  if (isUrlAttr) {
54
57
  const sanitized = sanitizeUrl(String(value));
55
58
  if (sanitized === null) {
56
- console.warn(
57
- `[Pulse Security] Dangerous URL blocked in bind() for ${attr}: "${String(value).slice(0, 50)}"`
59
+ log.warn(
60
+ `[Security] Dangerous URL blocked in bind() for ${attr}: "${String(value).slice(0, 50)}"`
58
61
  );
59
62
  dom.removeAttribute(element, attr);
60
63
  return;
@@ -70,8 +73,8 @@ export function bind(element, attr, getValue) {
70
73
  if (isUrlAttr) {
71
74
  const sanitized = sanitizeUrl(String(getValue));
72
75
  if (sanitized === null) {
73
- console.warn(
74
- `[Pulse Security] Dangerous URL blocked in bind() for ${attr}: "${String(getValue).slice(0, 50)}"`
76
+ log.warn(
77
+ `[Security] Dangerous URL blocked in bind() for ${attr}: "${String(getValue).slice(0, 50)}"`
75
78
  );
76
79
  return element;
77
80
  }
@@ -17,6 +17,193 @@ const log = loggers.dom;
17
17
  // ELEMENT CREATION
18
18
  // =============================================================================
19
19
 
20
+ // A11y configuration
21
+ let a11yConfig = {
22
+ enabled: true,
23
+ autoAria: true,
24
+ warnMissingAlt: true,
25
+ warnMissingLabel: true
26
+ };
27
+
28
+ /**
29
+ * Configure accessibility features
30
+ * @param {object} config - A11y configuration
31
+ */
32
+ export function configureA11y(config) {
33
+ a11yConfig = { ...a11yConfig, ...config };
34
+ }
35
+
36
+ /**
37
+ * Apply automatic ARIA attributes based on element type
38
+ * @private
39
+ */
40
+ function applyAutoAria(element, tag, attrs, dom) {
41
+ if (!a11yConfig.enabled || !a11yConfig.autoAria) return;
42
+
43
+ const hasAttr = (name) => attrs[name] !== undefined || dom.getAttribute?.(element, name);
44
+
45
+ switch (tag) {
46
+ case 'dialog':
47
+ // Dialogs should have role and modal indication
48
+ if (!hasAttr('role')) {
49
+ safeSetAttribute(element, 'role', 'dialog', {}, dom);
50
+ }
51
+ if (!hasAttr('aria-modal')) {
52
+ safeSetAttribute(element, 'aria-modal', 'true', {}, dom);
53
+ }
54
+ break;
55
+
56
+ case 'nav':
57
+ // Navigation landmarks benefit from labels
58
+ if (!hasAttr('aria-label') && !hasAttr('aria-labelledby') && a11yConfig.warnMissingLabel) {
59
+ log.warn('A11y: <nav> element should have aria-label or aria-labelledby for accessibility');
60
+ }
61
+ break;
62
+
63
+ case 'main':
64
+ case 'header':
65
+ case 'footer':
66
+ case 'aside':
67
+ // Landmark roles - warn if multiple without labels
68
+ if (!hasAttr('aria-label') && !hasAttr('aria-labelledby')) {
69
+ // These are valid as landmarks, just note for multiple instances
70
+ }
71
+ break;
72
+
73
+ case 'img':
74
+ // Images must have alt
75
+ if (!hasAttr('alt') && !hasAttr('aria-label') && !hasAttr('aria-hidden') && a11yConfig.warnMissingAlt) {
76
+ log.warn('A11y: <img> element missing alt attribute. Add alt="" for decorative images.');
77
+ }
78
+ break;
79
+
80
+ case 'input':
81
+ case 'textarea':
82
+ case 'select':
83
+ // Form controls need labels
84
+ if (!hasAttr('aria-label') && !hasAttr('aria-labelledby') && !hasAttr('id') && a11yConfig.warnMissingLabel) {
85
+ const inputType = attrs.type || 'text';
86
+ if (!['hidden', 'submit', 'button', 'reset', 'image'].includes(inputType)) {
87
+ log.warn(`A11y: <${tag}> element should have aria-label, aria-labelledby, or an associated <label>`);
88
+ }
89
+ }
90
+ break;
91
+
92
+ case 'button':
93
+ // Buttons are already focusable, ensure type if not specified
94
+ if (!hasAttr('type')) {
95
+ safeSetAttribute(element, 'type', 'button', {}, dom);
96
+ }
97
+ break;
98
+
99
+ case 'a':
100
+ // Links without href should have role="button" if interactive
101
+ if (!hasAttr('href') && !hasAttr('role')) {
102
+ safeSetAttribute(element, 'role', 'button', {}, dom);
103
+ if (!hasAttr('tabindex')) {
104
+ safeSetAttribute(element, 'tabindex', '0', {}, dom);
105
+ }
106
+ }
107
+ break;
108
+
109
+ case 'ul':
110
+ case 'ol':
111
+ // Lists with role="menu" or "listbox" need special handling
112
+ if (attrs.role === 'menu' || attrs.role === 'listbox') {
113
+ if (!hasAttr('aria-label') && !hasAttr('aria-labelledby')) {
114
+ log.warn(`A11y: <${tag} role="${attrs.role}"> should have aria-label or aria-labelledby`);
115
+ }
116
+ }
117
+ break;
118
+
119
+ case 'table':
120
+ // Tables benefit from captions or aria-label
121
+ if (!hasAttr('aria-label') && !hasAttr('aria-labelledby')) {
122
+ // Will be checked if no <caption> child is found later
123
+ }
124
+ break;
125
+
126
+ case 'progress':
127
+ // Progress should have accessible name
128
+ if (!hasAttr('aria-label') && !hasAttr('aria-labelledby')) {
129
+ log.warn('A11y: <progress> element should have aria-label or aria-labelledby');
130
+ }
131
+ break;
132
+
133
+ case 'meter':
134
+ // Meter should have accessible name
135
+ if (!hasAttr('aria-label') && !hasAttr('aria-labelledby')) {
136
+ log.warn('A11y: <meter> element should have aria-label or aria-labelledby');
137
+ }
138
+ break;
139
+ }
140
+
141
+ // Handle role-specific requirements
142
+ const role = attrs.role;
143
+ if (role) {
144
+ applyRoleRequirements(element, role, attrs, dom);
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Apply ARIA requirements for specific roles
150
+ * @private
151
+ */
152
+ function applyRoleRequirements(element, role, attrs, dom) {
153
+ const hasAttr = (name) => attrs[name] !== undefined;
154
+
155
+ switch (role) {
156
+ case 'checkbox':
157
+ case 'radio':
158
+ case 'switch':
159
+ if (!hasAttr('aria-checked')) {
160
+ safeSetAttribute(element, 'aria-checked', 'false', {}, dom);
161
+ }
162
+ break;
163
+
164
+ case 'slider':
165
+ case 'spinbutton':
166
+ case 'progressbar':
167
+ if (!hasAttr('aria-valuenow')) {
168
+ safeSetAttribute(element, 'aria-valuenow', '0', {}, dom);
169
+ }
170
+ if (!hasAttr('aria-valuemin')) {
171
+ safeSetAttribute(element, 'aria-valuemin', '0', {}, dom);
172
+ }
173
+ if (!hasAttr('aria-valuemax')) {
174
+ safeSetAttribute(element, 'aria-valuemax', '100', {}, dom);
175
+ }
176
+ break;
177
+
178
+ case 'combobox':
179
+ if (!hasAttr('aria-expanded')) {
180
+ safeSetAttribute(element, 'aria-expanded', 'false', {}, dom);
181
+ }
182
+ break;
183
+
184
+ case 'tablist':
185
+ if (!hasAttr('aria-orientation')) {
186
+ safeSetAttribute(element, 'aria-orientation', 'horizontal', {}, dom);
187
+ }
188
+ break;
189
+
190
+ case 'tab':
191
+ if (!hasAttr('aria-selected')) {
192
+ safeSetAttribute(element, 'aria-selected', 'false', {}, dom);
193
+ }
194
+ break;
195
+
196
+ case 'button':
197
+ case 'link':
198
+ case 'menuitem':
199
+ // Ensure focusability for interactive roles on non-focusable elements
200
+ if (!hasAttr('tabindex')) {
201
+ safeSetAttribute(element, 'tabindex', '0', {}, dom);
202
+ }
203
+ break;
204
+ }
205
+ }
206
+
20
207
  /**
21
208
  * Create a DOM element from a CSS selector-like string
22
209
  *
@@ -42,6 +229,9 @@ export function el(selector, ...children) {
42
229
  safeSetAttribute(element, key, value, {}, dom);
43
230
  }
44
231
 
232
+ // Apply automatic ARIA attributes based on element type
233
+ applyAutoAria(element, config.tag, config.attrs, dom);
234
+
45
235
  // Process children
46
236
  for (const child of children) {
47
237
  appendChild(element, child);
@@ -138,5 +328,6 @@ export function text(getValue) {
138
328
 
139
329
  export default {
140
330
  el,
141
- text
331
+ text,
332
+ configureA11y
142
333
  };
package/runtime/dom.js CHANGED
@@ -38,8 +38,8 @@ import {
38
38
  resetCacheMetrics
39
39
  } from './dom-selector.js';
40
40
 
41
- // Core element creation
42
- import { el, text } from './dom-element.js';
41
+ // Core element creation and a11y configuration
42
+ import { el, text, configureA11y } from './dom-element.js';
43
43
 
44
44
  // Reactive bindings
45
45
  import { bind, prop, cls, style, on, model } from './dom-binding.js';
@@ -99,6 +99,9 @@ export {
99
99
  el,
100
100
  text,
101
101
 
102
+ // Accessibility
103
+ configureA11y,
104
+
102
105
  // Reactive bindings
103
106
  bind,
104
107
  prop,
@@ -140,6 +143,9 @@ export default {
140
143
  el,
141
144
  text,
142
145
 
146
+ // Accessibility
147
+ configureA11y,
148
+
143
149
  // Reactive bindings
144
150
  bind,
145
151
  prop,
package/runtime/index.js CHANGED
@@ -8,6 +8,7 @@ export * from './router.js';
8
8
  export * from './store.js';
9
9
  export * from './native.js';
10
10
  export * from './logger.js';
11
+ export * from './a11y.js';
11
12
 
12
13
  export { default as PulseCore } from './pulse.js';
13
14
  export { default as PulseDOM } from './dom.js';
@@ -15,3 +16,4 @@ export { default as PulseRouter } from './router.js';
15
16
  export { default as PulseStore } from './store.js';
16
17
  export { default as PulseNative } from './native.js';
17
18
  export { default as PulseLogger } from './logger.js';
19
+ export { default as PulseA11y } from './a11y.js';
package/runtime/native.js CHANGED
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { pulse, effect, batch } from './pulse.js';
11
11
  import { loggers } from './logger.js';
12
+ import { DANGEROUS_KEYS } from './security.js';
12
13
 
13
14
  const log = loggers.native;
14
15
 
@@ -205,8 +206,7 @@ function _validateBridge(bridge) {
205
206
  }
206
207
 
207
208
  // 7. Check for suspicious properties (potential tampering)
208
- const suspiciousProps = ['eval', 'Function', '__proto__', 'constructor'];
209
- for (const prop of suspiciousProps) {
209
+ for (const prop of DANGEROUS_KEYS) {
210
210
  if (prop in bridge && typeof bridge[prop] === 'function') {
211
211
  warnings.push(`Suspicious property detected on bridge: ${prop}`);
212
212
  }