native-document 1.0.169 → 1.0.172

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.
Files changed (123) hide show
  1. package/CHANGELOG.md +62 -1
  2. package/docs/vitepress-conventions.md +2 -2
  3. package/package.json +11 -4
  4. package/src/components/BaseComponent.js +43 -1
  5. package/src/components/accordion/Accordion.js +1 -0
  6. package/src/components/accordion/AccordionItem.js +1 -0
  7. package/src/components/alert/Alert.js +4 -0
  8. package/src/components/avatar/Avatar.js +1 -0
  9. package/src/components/avatar/AvatarGroup.js +1 -0
  10. package/src/components/badge/Badge.js +1 -0
  11. package/src/components/breadcrumb/BreadCrumb.js +4 -0
  12. package/src/components/button/Button.js +1 -0
  13. package/src/components/card/Card.js +1 -0
  14. package/src/components/context-menu/ContextMenu.js +2 -0
  15. package/src/components/context-menu/ContextMenuGroup.js +2 -0
  16. package/src/components/context-menu/ContextMenuItem.js +2 -0
  17. package/src/components/divider/Divider.js +2 -0
  18. package/src/components/dropdown/Dropdown.js +1 -0
  19. package/src/components/dropdown/DropdownDivider.js +1 -0
  20. package/src/components/dropdown/DropdownGroup.js +1 -0
  21. package/src/components/dropdown/DropdownItem.js +1 -0
  22. package/src/components/dropdown/DropdownTrigger.js +1 -0
  23. package/src/components/form/FormControl.js +1 -0
  24. package/src/components/form/field/Field.js +1 -0
  25. package/src/components/form/field/types/StringField.js +1 -1
  26. package/src/components/icon/Icon.js +1 -0
  27. package/src/components/list/ListItem.js +1 -0
  28. package/src/components/menu/Menu.js +3 -0
  29. package/src/components/menu/MenuDivider.js +1 -0
  30. package/src/components/menu/MenuGroup.js +1 -0
  31. package/src/components/menu/MenuItem.js +1 -0
  32. package/src/components/menu/MenuLink.js +1 -0
  33. package/src/components/modal/Modal.js +4 -0
  34. package/src/components/pagination/Pagination.js +1 -0
  35. package/src/components/popover/Popover.js +1 -0
  36. package/src/components/popover/PopoverFooter.js +1 -0
  37. package/src/components/popover/PopoverHeader.js +1 -0
  38. package/src/components/progress/Progress.js +5 -0
  39. package/src/components/skeleton/Skeleton.js +4 -0
  40. package/src/components/slider/Slider.js +1 -0
  41. package/src/components/spacer/Spacer.js +1 -0
  42. package/src/components/spinner/Spinner.js +1 -0
  43. package/src/components/splitter/Splitter.js +1 -0
  44. package/src/components/splitter/SplitterGutter.js +6 -0
  45. package/src/components/splitter/SplitterPanel.js +1 -0
  46. package/src/components/stacks/AbsoluteStack.js +1 -0
  47. package/src/components/stacks/FixedStack.js +1 -0
  48. package/src/components/stacks/HStack.js +1 -0
  49. package/src/components/stacks/PositionStack.js +1 -0
  50. package/src/components/stacks/RelativeStack.js +1 -0
  51. package/src/components/stacks/Stack.js +1 -0
  52. package/src/components/stacks/VStack.js +1 -0
  53. package/src/components/stepper/Stepper.js +1 -0
  54. package/src/components/stepper/StepperStep.js +1 -0
  55. package/src/components/switch/Switch.js +1 -0
  56. package/src/components/table/DataTable.js +1 -0
  57. package/src/components/table/SimpleTable.js +1 -0
  58. package/src/components/tabs/Tabs.js +1 -0
  59. package/src/components/toast/Toast.js +1 -0
  60. package/src/components/tooltip/Tooltip.js +1 -0
  61. package/src/core/data/MemoryManager.js +6 -1
  62. package/src/core/data/ObservableItem.js +28 -10
  63. package/src/core/elements/anchor/anchor.js +4 -3
  64. package/src/core/elements/anchor/one-child-anchor-overwriting.js +4 -4
  65. package/src/core/elements/content-formatter.js +19 -0
  66. package/src/core/elements/control/for-each-array-cache.js +59 -0
  67. package/src/core/elements/control/for-each-array.js +15 -23
  68. package/src/core/wrappers/AttributesWrapper.js +12 -12
  69. package/src/core/wrappers/ElementCreator.js +33 -10
  70. package/src/core/wrappers/HtmlElementWrapper.js +9 -4
  71. package/src/core/wrappers/NDElement.js +23 -8
  72. package/src/core/wrappers/NdPrototype.js +31 -39
  73. package/src/core/wrappers/constants.js +12 -12
  74. package/src/core/wrappers/prototypes/nd-element-extensions.js +127 -57
  75. package/src/ui/components/accordion/AccordionItemRender.js +29 -7
  76. package/src/ui/components/alert/AlertRender.js +6 -0
  77. package/src/ui/components/breadcrumb/BreadcrumbRender.js +9 -1
  78. package/src/ui/components/button/ButtonRender.js +3 -0
  79. package/src/ui/components/contextmenu/ContextmenuRender.js +2 -0
  80. package/src/ui/components/dropdown/DropdownRender.js +3 -0
  81. package/src/ui/components/dropdown/item/DropdownItemRender.js +6 -1
  82. package/src/ui/components/form/FieldCollectionRender.js +4 -0
  83. package/src/ui/components/form/fields/CheckboxFieldRender.js +4 -0
  84. package/src/ui/components/form/fields/CheckboxGroupFieldRender.js +4 -0
  85. package/src/ui/components/form/fields/RadioFieldRender.js +4 -0
  86. package/src/ui/components/form/fields/RangeFieldRender.js +4 -0
  87. package/src/ui/components/form/fields/SelectFieldRender.js +5 -0
  88. package/src/ui/components/form/fields/SliderFieldRender.js +3 -0
  89. package/src/ui/components/icon/tabler/TablerIconRender.js +0 -1
  90. package/src/ui/components/list/item/ListItemRender.js +4 -0
  91. package/src/ui/components/menu/MenuItemRender.js +8 -0
  92. package/src/ui/components/menu/MenuLinkRender.js +11 -0
  93. package/src/ui/components/menu/MenuRender.js +5 -0
  94. package/src/ui/components/menu/helpers.js +2 -1
  95. package/src/ui/components/modal/ModalRender.js +8 -2
  96. package/src/ui/components/progress/ProgressRender.js +9 -0
  97. package/src/ui/components/skeleton/SkeletonRender.js +2 -0
  98. package/src/ui/components/spinner/SpinnerRender.js +3 -0
  99. package/src/ui/components/splitter/SplitterGutterRender.js +4 -0
  100. package/src/ui/components/splitter/SplitterPanelRender.js +4 -0
  101. package/src/ui/components/splitter/SplitterRender.js +2 -0
  102. package/src/ui/components/stepper/StepperRender.js +2 -1
  103. package/src/ui/components/stepper/StepperStepRender.js +8 -1
  104. package/src/ui/components/switch/SwitchRender.js +3 -0
  105. package/src/ui/components/table/data-table/DataTableRender.js +4 -0
  106. package/src/ui/components/table/simple-table/SimpleTableRender.js +2 -0
  107. package/src/ui/components/toast/ToastRender.js +4 -0
  108. package/src/ui/utils/aria.js +19 -0
  109. package/tests/integration/ui/attributes-wrapper.test.js +177 -0
  110. package/tests/integration/ui/for-each-array.test.js +167 -0
  111. package/tests/integration/ui/lifecycle.test.js +146 -0
  112. package/tests/integration/ui/show-if.test.js +168 -0
  113. package/tests/unit/core/observable-array.test.js +404 -0
  114. package/tests/unit/core/observable-item.test.js +428 -0
  115. package/tests/unit/core/observable-object.test.js +239 -0
  116. package/tests/unit/core/observable-resource.test.js +323 -0
  117. package/tests/unit/core/store.test.js +347 -0
  118. package/tests/unit/router/route-matching.test.js +169 -0
  119. package/tests/unit/router/router-guards.test.js +189 -0
  120. package/tests/unit/utils/cache.test.js +120 -0
  121. package/tests/unit/utils/filters.test.js +394 -0
  122. package/types/elements.d.ts +5 -0
  123. package/vitest.config.js +15 -0
@@ -187,16 +187,32 @@ NDElement.prototype.destroyOnUnmount = function() {
187
187
  * @returns {this}
188
188
  */
189
189
  NDElement.prototype.destroy = function() {
190
- this.$element?.querySelectorAll('[data--nd-before-unmount]').forEach(child => {
190
+ if(!this.$element) {
191
+ return this;
192
+ }
193
+ const observer = $lifeCycleObservers.get(this.$element);
194
+ if(observer) {
195
+ observer.disconnect();
196
+ }
197
+
198
+ const treewalk = document.createTreeWalker(this.$element, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT);
199
+ const nodes = [];
200
+ while(treewalk.nextNode()) {
201
+ nodes.push(treewalk.currentNode);
202
+ }
203
+
204
+ for(let i = 0, length = nodes.length; i < length; i++) {
205
+ const child = nodes[i];
191
206
  child.remove();
192
- child.__$controller?.abort();
193
- child.__$controller = null;
207
+ const childObserver = $lifeCycleObservers.get(child);
208
+ if(childObserver) {
209
+ childObserver.disconnect();
210
+ }
194
211
  $lifeCycleObservers.delete(child);
195
- });
212
+ }
196
213
 
197
- this.$element.__$controller?.abort();
198
- this.$element.__$controller = null;
199
214
  $lifeCycleObservers.delete(this.$element);
215
+ this.$element.remove();
200
216
  this.$element = null;
201
217
  };
202
218
 
@@ -418,7 +434,6 @@ NDElement.prototype.style = function(style) {
418
434
  return this;
419
435
  };
420
436
 
421
-
422
437
  /**
423
438
  * Extends the NDElement prototype with new methods available to all NDElement instances.
424
439
  * Use this to add global methods to all NDElements.
@@ -618,7 +633,7 @@ NDElement.prototype.contentEditable = function($obs, {format = 'html'} = {}) {
618
633
 
619
634
  this.$element.addEventListener('input', () => {
620
635
  $obs?.set(getValue());
621
- }, { signal: this.$getSignal() });
636
+ });
622
637
 
623
638
  return this;
624
639
  };
@@ -20,65 +20,42 @@ Object.defineProperty(NDElement.prototype, 'nd', {
20
20
  });
21
21
 
22
22
 
23
-
24
23
  // ----------------------------------------------------------------
25
24
  // Events helpers
26
25
  // ----------------------------------------------------------------
27
26
  EVENTS.forEach(eventSourceName => {
28
27
  const eventName = eventSourceName.toLowerCase();
29
- NDElement.prototype['on'+eventSourceName] = function(callback = null, options = {}) {
30
- this.$element.addEventListener(eventName, callback, {
31
- signal: this.$getSignal(),
32
- ...options,
33
- });
28
+ const inlineHandler = 'on'+eventName;
29
+ NDElement.prototype['on'+eventSourceName] = function(callback = null, options = null) {
30
+ if(!this.$element[inlineHandler] && !options) {
31
+ this.$element[inlineHandler] = callback;
32
+ return this;
33
+ }
34
+ this.$element.addEventListener(eventName, callback, options);
34
35
  return this;
35
36
  };
36
37
  });
37
38
 
38
39
  EVENTS_WITH_STOP.forEach(eventSourceName => {
39
40
  const eventName = eventSourceName.toLowerCase();
40
- NDElement.prototype['onStop'+eventSourceName] = function(callback = null, options = {}) {
41
- _stop(this.$element, eventName, callback, {
42
- signal: this.$getSignal(),
43
- ...options,
44
- });
41
+ NDElement.prototype['onStop'+eventSourceName] = function(callback = null, options = null) {
42
+ _stop(this.$element, eventName, callback, options);
45
43
  return this;
46
44
  };
47
- NDElement.prototype['onPreventStop'+eventSourceName] = function(callback = null, options = {}) {
48
- _preventStop(this.$element, eventName, callback, {
49
- signal: this.$getSignal(),
50
- ...options,
51
- });
45
+ NDElement.prototype['onPreventStop'+eventSourceName] = function(callback = null, options = null) {
46
+ _preventStop(this.$element, eventName, callback, options);
52
47
  return this;
53
48
  };
54
49
  });
55
50
 
56
51
  EVENTS_WITH_PREVENT.forEach(eventSourceName => {
57
52
  const eventName = eventSourceName.toLowerCase();
58
- NDElement.prototype['onPrevent'+eventSourceName] = function(callback = null, options = {}) {
59
- _prevent(this.$element, eventName, callback, {
60
- signal: this.$getSignal(),
61
- ...options,
62
- });
53
+ NDElement.prototype['onPrevent'+eventSourceName] = function(callback = null, options = null) {
54
+ _prevent(this.$element, eventName, callback, options);
63
55
  return this;
64
56
  };
65
57
  });
66
58
 
67
- /**
68
- * Retrieves or creates an AbortController signal tied to this element's lifecycle.
69
- * The signal is automatically aborted when the element is removed via .nd.remove().
70
- * Used internally by all event listeners (onClick, onInput, etc.) to auto-cleanup on unmount.
71
- *
72
- * @internal
73
- * @returns {AbortSignal} The signal for this element's AbortController
74
- */
75
- NDElement.prototype.$getSignal = function() {
76
- if(!this.$element.__$controller) {
77
- this.$element.__$controller = new AbortController();
78
- }
79
- return this.$element.__$controller.signal;
80
- };
81
-
82
59
 
83
60
  /**
84
61
  * Adds a native event listener to the underlying HTMLElement.
@@ -91,7 +68,6 @@ NDElement.prototype.$getSignal = function() {
91
68
  */
92
69
  NDElement.prototype.on = function(name, callback, options) {
93
70
  this.$element.addEventListener(name.toLowerCase(), callback, {
94
- signal: this.$getSignal(),
95
71
  ...options,
96
72
  });
97
73
  return this;
@@ -119,7 +95,6 @@ NDElement.prototype.off = function(name, callback) {
119
95
  */
120
96
  NDElement.prototype.once = function(name, callback) {
121
97
  this.$element.addEventListener(name.toLowerCase(), callback, {
122
- signal: this.$getSignal(),
123
98
  once: true,
124
99
  });
125
100
  return this;
@@ -151,8 +126,13 @@ const _prevent = function(element, eventName, callback, options) {
151
126
  event.preventDefault();
152
127
  callback && callback.call(element, event);
153
128
  };
129
+
130
+ const inlineHandler = 'on' + eventName;
131
+ if(!element[inlineHandler] && !options) {
132
+ element[inlineHandler] = handler;
133
+ return;
134
+ }
154
135
  element.addEventListener(eventName, handler, options);
155
- return this;
156
136
  };
157
137
 
158
138
  const _stop = function(element, eventName, callback, options) {
@@ -160,6 +140,13 @@ const _stop = function(element, eventName, callback, options) {
160
140
  event.stopPropagation();
161
141
  callback && callback.call(element, event);
162
142
  };
143
+
144
+ const inlineHandler = 'on' + eventName;
145
+ if(!element[inlineHandler] && !options) {
146
+ element[inlineHandler] = handler;
147
+ return;
148
+ }
149
+
163
150
  element.addEventListener(eventName, handler, options);
164
151
  return this;
165
152
  };
@@ -170,6 +157,11 @@ const _preventStop = function(element, eventName, callback, options) {
170
157
  event.preventDefault();
171
158
  callback && callback.call(element, event);
172
159
  };
160
+ const inlineHandler = 'on' + eventName;
161
+ if(!element[inlineHandler] && !options) {
162
+ element[inlineHandler] = handler;
163
+ return;
164
+ }
173
165
  element.addEventListener(eventName, handler, options);
174
166
  return this;
175
167
  };
@@ -3,19 +3,19 @@ export const BOOLEAN_ATTRIBUTES = new Set([
3
3
  'checked',
4
4
  'selected',
5
5
  'disabled',
6
- 'readonly',
6
+ 'readonly', 'readOnly',
7
7
  'required',
8
- 'autofocus',
8
+ 'autofocus', 'autoFocus',
9
9
  'multiple',
10
- 'autocomplete',
10
+ 'autocomplete', 'autoComplete',
11
11
  'hidden',
12
- 'contenteditable',
13
- 'spellcheck',
12
+ 'contenteditable', 'contentEditable',
13
+ 'spellcheck', 'spellCheck',
14
14
  'translate',
15
15
  'draggable',
16
16
  'async',
17
17
  'defer',
18
- 'autoplay',
18
+ 'autoplay', 'autoPlay',
19
19
  'controls',
20
20
  'loop',
21
21
  'muted',
@@ -23,13 +23,13 @@ export const BOOLEAN_ATTRIBUTES = new Set([
23
23
  'reversed',
24
24
  'open',
25
25
  'default',
26
- 'formnovalidate',
27
- 'novalidate',
26
+ 'formnovalidate', 'formNoValidate',
27
+ 'novalidate', 'noValidate',
28
28
  'scoped',
29
- 'itemscope',
30
- 'allowfullscreen',
31
- 'allowpaymentrequest',
32
- 'playsinline',
29
+ 'itemscope', 'itemScope',
30
+ 'allowfullscreen', 'allowFullscreen',
31
+ 'allowpaymentrequest', 'allowPaymentRequest',
32
+ 'playsinline', 'playsInline',
33
33
  ]);
34
34
 
35
35
  export const BOOL_ATTRIBUTES_NAME = {
@@ -1,103 +1,174 @@
1
1
  import ObservableItem from '../../data/ObservableItem';
2
- import {NDElement} from '../NDElement';
2
+ import { NDElement } from '../NDElement';
3
3
  import TemplateBinding from '../TemplateBinding';
4
- import {ElementCreator} from '../ElementCreator';
4
+ import { ElementCreator } from '../ElementCreator';
5
5
  import PluginsManager from '../../utils/plugins-manager';
6
6
  import ObservableChecker from '../../data/ObservableChecker';
7
+ import MemoryManager from '../../data/MemoryManager';
7
8
 
8
9
 
9
- NDElement.$getChild = ElementCreator.getChild;
10
+
11
+ const OBS_PATTERN = /({{obs:\d+}})/;
12
+ const OBS_ID_PATTERN = /\d+/;
13
+
10
14
 
11
15
  /**
12
- * Converts a string to a reactive text node.
16
+ * Parses a string containing Observable placeholders and returns an array
17
+ * of strings and Observable instances.
13
18
  *
14
- * @returns {Text} Static text node containing the string value
19
+ * Placeholders are generated automatically when an ObservableItem is interpolated
20
+ * in a template string via its toString() method.
21
+ *
22
+ * @returns {Array<string|ObservableItem>} Mixed array of static strings and resolved Observables
23
+ * @example
24
+ * const name = Observable('John');
25
+ * const count = Observable(5);
26
+ *
27
+ * `Hello ${name}, count: ${count}`.toNdChildren()
28
+ * // → ['Hello ', ObservableItem(John), ', count: ', ObservableItem(5)]
15
29
  */
16
- String.prototype.toNdElement = function () {
17
- return ElementCreator.createStaticTextNode(null, this);
18
- };
30
+ Object.defineProperty(String.prototype, 'toNdChildren', {
31
+ value: function() {
32
+ return this.split(OBS_PATTERN).filter(Boolean).map((item) => {
33
+ if(OBS_PATTERN.test(item)) {
34
+ const id = item.match(OBS_ID_PATTERN)[0];
35
+ return MemoryManager.getObservableById(Number(id));
36
+ }
37
+ return item;
38
+ });
39
+ },
40
+ writable: true,
41
+ configurable: true,
42
+ enumerable: false,
43
+ });
19
44
 
20
45
  /**
21
- * Converts a number to a static text node.
46
+ * Registers a non-enumerable toNdElement method on a prototype.
22
47
  *
23
- * @returns {Text} Static text node containing the number as a string
48
+ * @param {object} target - The prototype to extend
49
+ * @param {Function} fn - The toNdElement implementation
24
50
  */
25
- Number.prototype.toNdElement = function () {
26
- return ElementCreator.createStaticTextNode(null, this.toString());
51
+ const defineToNdElement = (target, fn) => {
52
+ Object.defineProperty(target, 'toNdElement', {
53
+ value: fn,
54
+ writable: true,
55
+ configurable: true,
56
+ enumerable: false,
57
+ });
27
58
  };
28
59
 
29
60
  /**
30
- * Returns the element itself (identity for DOM compatibility).
61
+ * Marks a prototype as a valid NativeDocument child node.
31
62
  *
32
- * @returns {Element} this
63
+ * @param {object} target - The prototype to mark
33
64
  */
34
- Element.prototype.toNdElement = function () {
35
- return this;
65
+ const defineValidNdChild = (target) => {
66
+ Object.defineProperty(target, '__$isValidNdChild', {
67
+ value: true,
68
+ writable: true,
69
+ configurable: true,
70
+ enumerable: false,
71
+ });
36
72
  };
37
73
 
38
74
  /**
39
- * Returns the text node itself (identity for DOM compatibility).
75
+ * Marks a prototype as a native DOM node.
76
+ * Faster alternative to instanceof Node for hot paths.
40
77
  *
41
- * @returns {Text} this
78
+ * @param {object} target - The prototype to mark
42
79
  */
43
- Text.prototype.toNdElement = function () {
44
- return this;
80
+ const defineNativeNode = (target) => {
81
+ Object.defineProperty(target, '__$isNativeNode', {
82
+ value: true,
83
+ writable: true,
84
+ configurable: true,
85
+ enumerable: false,
86
+ });
45
87
  };
46
88
 
89
+ NDElement.$getChild = ElementCreator.getChild;
90
+
91
+ // -- __$isNativeNode ----------------------------------------------------------
92
+
93
+ defineNativeNode(Element.prototype);
94
+ defineNativeNode(Text.prototype);
95
+ defineNativeNode(Comment.prototype);
96
+ defineNativeNode(Document.prototype);
97
+ defineNativeNode(DocumentFragment.prototype);
98
+
99
+ // -- __$isValidNdChild --------------------------------------------------------
100
+
101
+ defineValidNdChild(String.prototype);
102
+ defineValidNdChild(Number.prototype);
103
+ defineValidNdChild(Boolean.prototype);
104
+ defineValidNdChild(Array.prototype);
105
+ defineValidNdChild(Element.prototype);
106
+ defineValidNdChild(Text.prototype);
107
+ defineValidNdChild(Comment.prototype);
108
+ defineValidNdChild(DocumentFragment.prototype);
109
+ defineValidNdChild(NDElement.prototype);
110
+ defineValidNdChild(ObservableItem.prototype);
111
+ defineValidNdChild(ObservableChecker.prototype);
112
+ defineValidNdChild(TemplateBinding.prototype);
113
+ defineValidNdChild(Function.prototype);
114
+
115
+ // -- toNdElement --------------------------------------------------------------
116
+
47
117
  /**
48
- * Returns the comment node itself (identity for DOM compatibility).
118
+ * Converts a string to a static text node.
49
119
  *
50
- * @returns {Comment} this
120
+ * @returns {Text}
51
121
  */
52
- Comment.prototype.toNdElement = function () {
53
- return this;
54
- };
122
+ defineToNdElement(String.prototype, function () {
123
+ return ElementCreator.createStaticTextNodeWithoutParent(this);
124
+ });
55
125
 
56
126
  /**
57
- * Returns the document itself (identity for DOM compatibility).
127
+ * Converts a number to a static text node.
58
128
  *
59
- * @returns {Document} this
129
+ * @returns {Text}
60
130
  */
61
- Document.prototype.toNdElement = function () {
62
- return this;
63
- };
131
+ defineToNdElement(Number.prototype, function () {
132
+ return ElementCreator.createStaticTextNodeWithoutParent(this.toString());
133
+ });
64
134
 
65
135
  /**
66
- * Returns the document fragment itself (identity for DOM compatibility).
136
+ * Returns the DOM node itself (identity).
67
137
  *
68
- * @returns {DocumentFragment} this
138
+ * @returns {Element|Text|Comment|Document|DocumentFragment}
69
139
  */
70
- DocumentFragment.prototype.toNdElement = function () {
71
- return this;
72
- };
140
+ defineToNdElement(Element.prototype, function () { return this; });
141
+ defineToNdElement(Text.prototype, function () { return this; });
142
+ defineToNdElement(Comment.prototype, function () { return this; });
143
+ defineToNdElement(Document.prototype, function () { return this; });
144
+ defineToNdElement(DocumentFragment.prototype, function () { return this; });
73
145
 
74
146
  /**
75
- * Converts the ObservableItem to a reactive text node that updates automatically when the value changes.
147
+ * Converts an ObservableItem to a reactive text node that updates when the value changes.
76
148
  *
77
- * @returns {Text} Reactive text node bound to this observable
149
+ * @returns {Text}
78
150
  */
79
- ObservableItem.prototype.toNdElement = function () {
80
- return ElementCreator.createObservableNode(null, this);
81
- };
151
+ defineToNdElement(ObservableItem.prototype, function () {
152
+ return ElementCreator.createObservableNodeWithoutParent(this);
153
+ });
82
154
 
83
155
  ObservableChecker.prototype.toNdElement = ObservableItem.prototype.toNdElement;
84
156
 
85
157
  /**
86
- * Converts the NDElement to its underlying HTMLElement (or ghost DOM fragment if ghostDom was used).
158
+ * Returns the underlying HTMLElement of this NDElement.
87
159
  *
88
- * @returns {HTMLElement|DocumentFragment} The underlying DOM node
160
+ * @returns {HTMLElement|DocumentFragment}
89
161
  */
90
- NDElement.prototype.toNdElement = function () {
162
+ defineToNdElement(NDElement.prototype, function () {
91
163
  return this.$element;
92
- };
164
+ });
93
165
 
94
166
  /**
95
- * Converts the array to a DocumentFragment containing all elements.
96
- * Each item is processed through ElementCreator.getChild().
167
+ * Converts the array to a DocumentFragment containing all child elements.
97
168
  *
98
- * @returns {DocumentFragment} Fragment containing all array children
169
+ * @returns {DocumentFragment}
99
170
  */
100
- Array.prototype.toNdElement = function () {
171
+ defineToNdElement(Array.prototype, function () {
101
172
  const fragment = document.createDocumentFragment();
102
173
  for(let i = 0, length = this.length; i < length; i++) {
103
174
  const child = ElementCreator.getChild(this[i]);
@@ -105,27 +176,26 @@ Array.prototype.toNdElement = function () {
105
176
  fragment.appendChild(child);
106
177
  }
107
178
  return fragment;
108
- };
179
+ });
109
180
 
110
181
  /**
111
182
  * Calls the function and converts its return value to a DOM node.
112
- * Used internally by ElementCreator to process function-based children.
113
183
  *
114
- * @returns {Node} The DOM node returned by the function
184
+ * @returns {Node}
115
185
  */
116
- Function.prototype.toNdElement = function () {
186
+ defineToNdElement(Function.prototype, function () {
117
187
  const child = this;
118
188
  if(process.env.NODE_ENV === 'development') {
119
189
  PluginsManager.emit('BeforeProcessComponent', child);
120
190
  }
121
191
  return ElementCreator.getChild(child());
122
- };
192
+ });
123
193
 
124
194
  /**
125
- * Converts the TemplateBinding to a hydratable DOM node for use in TemplateCloner.
195
+ * Converts the TemplateBinding to a hydratable DOM node.
126
196
  *
127
- * @returns {Node} Hydratable node
197
+ * @returns {Node}
128
198
  */
129
- TemplateBinding.prototype.toNdElement = function () {
199
+ defineToNdElement(TemplateBinding.prototype, function () {
130
200
  return ElementCreator.createHydratableNode(null, this);
131
- };
201
+ });
@@ -1,18 +1,26 @@
1
1
  import {Div, Span, ShowIf} from '../../../core/elements';
2
2
 
3
+ // [a11y] ID counter for aria-controls
4
+ let _accordionIdCounter = 0;
5
+
3
6
  export default function AccordionItemRender($desc, instance) {
4
7
  const props = instance.getEditableProps();
5
8
 
6
9
  props.class.add('accordion-item');
7
10
  props.class.add({'is-disabled': $desc.disabled});
8
11
 
9
- const header = buildHeader($desc, instance);
10
- const content = buildContent($desc, instance);
12
+ // [a11y] Generate unique IDs for aria-controls / aria-labelledby
13
+ const accordionId = ++_accordionIdCounter;
14
+ const headerId = `accordion-header-${accordionId}`;
15
+ const panelId = `accordion-panel-${accordionId}`;
16
+
17
+ const header = buildHeader($desc, instance, headerId, panelId);
18
+ const content = buildContent($desc, instance, headerId, panelId);
11
19
 
12
20
  return Div(instance.resolveProps(), [header, content]);
13
21
  }
14
22
 
15
- const buildHeader = ($desc, instance) => {
23
+ const buildHeader = ($desc, instance, headerId, panelId) => {
16
24
  if($desc.renderHeader) {
17
25
  return $desc.renderHeader($desc, instance);
18
26
  }
@@ -27,7 +35,13 @@ const buildHeader = ($desc, instance) => {
27
35
  buildIndicator($desc),
28
36
  ];
29
37
 
30
- const header = Div({class: 'accordion-header'}, content);
38
+ const header = Div({
39
+ class: 'accordion-header',
40
+ id: headerId,
41
+ // [a11y] aria-expanded reactive, aria-controls points to panel
42
+ 'aria-expanded': $desc.expanded.transform((v) => String(v)),
43
+ 'aria-controls': panelId,
44
+ }, content);
31
45
 
32
46
  if(!$desc.disabled?.val()) {
33
47
  header.nd.onClick(() => {
@@ -50,14 +64,22 @@ const buildIndicator = ($desc) => {
50
64
  }, '▾');
51
65
  };
52
66
 
53
- const buildContent = ($desc, instance) => {
67
+ const buildContent = ($desc, instance, headerId, panelId) => {
68
+ // [a11y] role=region + aria-labelledby on panel
69
+ const panelProps = {
70
+ class: 'accordion-content',
71
+ id: panelId,
72
+ role: 'region',
73
+ 'aria-labelledby': headerId,
74
+ };
75
+
54
76
  if($desc.renderContent) {
55
77
  return ShowIf($desc.expanded, () =>
56
- Div({class: 'accordion-content'}, $desc.renderContent($desc, instance)),
78
+ Div(panelProps, $desc.renderContent($desc, instance)),
57
79
  );
58
80
  }
59
81
 
60
82
  return ShowIf($desc.expanded, () =>
61
- Div({class: 'accordion-content'}, $desc.content),
83
+ Div(panelProps, $desc.content),
62
84
  );
63
85
  };
@@ -10,6 +10,12 @@ export default function AlertRender($desc, instance) {
10
10
  props.class.add('is-' + ($desc.variant || 'info'));
11
11
  props.class.add('is-' + ($desc.appearance || 'bordered'));
12
12
 
13
+ // [a11y] aria-live: assertive for error/warning, polite for others
14
+ const ariaLive = ($desc.variant === 'error' || $desc.variant === 'danger' || $desc.variant === 'warning')
15
+ ? 'assertive'
16
+ : 'polite';
17
+ props['aria-live'] = ariaLive;
18
+
13
19
  const content = [];
14
20
 
15
21
  if($desc.showIcon && $desc.icon) {
@@ -6,6 +6,9 @@ export default function BreadcrumbRender($desc, instance) {
6
6
  const props = instance.getEditableProps();
7
7
  props.class.add('breadcrumb');
8
8
 
9
+ // [a11y] aria-label on nav
10
+ props['aria-label'] = (typeof $desc.label === 'string') ? $desc.label : 'Breadcrumb';
11
+
9
12
  return Nav(instance.resolveProps(),
10
13
  OrderedList({ class: 'breadcrumb-list' },
11
14
  ForEachArray($desc.items, (item) => buildItem(item, $desc, instance)),
@@ -40,5 +43,10 @@ const buildItem = (item, $desc, instance) => {
40
43
 
41
44
  content.push(buildSeparator($desc));
42
45
 
43
- return ListItem({class: 'breadcrumb-item'}, content);
46
+ // [a11y] aria-current on last item (no href = current page)
47
+ const isCurrent = !item.href;
48
+ return ListItem({
49
+ class: 'breadcrumb-item',
50
+ ...( isCurrent ? { 'aria-current': 'page' } : {} ),
51
+ }, content);
44
52
  };
@@ -36,6 +36,9 @@ export default function ButtonRender($desc, instance) {
36
36
  ...instance.resolveProps(),
37
37
  type: $desc.type || 'button',
38
38
  disabled: $desc.disabled,
39
+ // [a11y] aria-busy and aria-disabled
40
+ ...($desc.loading ? { 'aria-busy': $desc.loading } : {}),
41
+ ...($desc.disabled ? { 'aria-disabled': $desc.disabled } : {}),
39
42
  }, content);
40
43
  }
41
44
 
@@ -3,6 +3,7 @@ import {createPortal} from '../../../core/elements/anchor/anchor';
3
3
  import {computePosition, flip, shift} from '@floating-ui/dom';
4
4
 
5
5
  import './contextmenu.css';
6
+ import {ariaTrigger} from '../../utils/aria';
6
7
 
7
8
  export default function ContextMenuRender($desc, instance) {
8
9
  const $positionX = instance.$description.positionX, $positionY = instance.$description.positionY;
@@ -19,6 +20,7 @@ export default function ContextMenuRender($desc, instance) {
19
20
  createPortal(container, {name: 'context-menu'});
20
21
 
21
22
  if($desc.trigger && instance) {
23
+ ariaTrigger($desc.trigger, $desc.isOpen, 'menu');
22
24
  contextMenuHandler($desc.trigger, instance);
23
25
  }
24
26
 
@@ -4,9 +4,12 @@ import {createFilter} from '../../../core/utils/filters';
4
4
  import {normalizeDropdownItem} from '../../../components/dropdown/helpers';
5
5
 
6
6
  import './dropdown.css';
7
+ import {ariaTrigger} from '../../utils/aria';
7
8
 
8
9
  export default function DropdownRender($desc, instance) {
9
10
  $desc.content = buildDropdownContent($desc, instance);
11
+ ariaTrigger($desc.trigger, $desc.isOpen, 'listbox');
12
+
10
13
 
11
14
  return PopoverRender($desc, instance, 'dropdown');
12
15
  }
@@ -5,7 +5,12 @@ export default function DropdownItemRender($desc, instance) {
5
5
  const props = instance.getEditableProps();
6
6
 
7
7
  props.class.add('dropdown-item');
8
- // props.class.add({ 'is-disabled': $desc.disabled, 'is-selected': $desc.selected });
8
+ props.class.add({ 'is-disabled': $desc.disabled, 'is-selected': $desc.selected });
9
+
10
+ if($desc.selected != null) {
11
+ props['aria-selected'] = $desc.selected;
12
+ }
13
+
9
14
 
10
15
  const content = [];
11
16
  if($desc.renderContent) {
@@ -7,6 +7,10 @@ export default function FieldCollectionRender($desc, instance) {
7
7
  const props = instance.getEditableProps();
8
8
 
9
9
  props.class.add('field-collection');
10
+ // [a11y] aria-label if provided
11
+ if($desc.label) {
12
+ props['aria-label'] = $desc.label;
13
+ }
10
14
 
11
15
  const $items = $desc.value;
12
16
  const $itemsMap = new WeakMap();