native-document 1.0.169 → 1.0.173

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 (134) hide show
  1. package/CHANGELOG.md +100 -2
  2. package/docs/vitepress-conventions.md +2 -2
  3. package/index.d.ts +1 -1
  4. package/package.json +11 -4
  5. package/src/components/BaseComponent.js +43 -1
  6. package/src/components/accordion/Accordion.js +1 -0
  7. package/src/components/accordion/AccordionItem.js +1 -0
  8. package/src/components/alert/Alert.js +4 -0
  9. package/src/components/avatar/Avatar.js +1 -0
  10. package/src/components/avatar/AvatarGroup.js +1 -0
  11. package/src/components/badge/Badge.js +1 -0
  12. package/src/components/breadcrumb/BreadCrumb.js +4 -0
  13. package/src/components/button/Button.js +1 -0
  14. package/src/components/card/Card.js +1 -0
  15. package/src/components/context-menu/ContextMenu.js +2 -0
  16. package/src/components/context-menu/ContextMenuGroup.js +2 -0
  17. package/src/components/context-menu/ContextMenuItem.js +2 -0
  18. package/src/components/divider/Divider.js +2 -0
  19. package/src/components/dropdown/Dropdown.js +1 -0
  20. package/src/components/dropdown/DropdownDivider.js +1 -0
  21. package/src/components/dropdown/DropdownGroup.js +1 -0
  22. package/src/components/dropdown/DropdownItem.js +1 -0
  23. package/src/components/dropdown/DropdownTrigger.js +1 -0
  24. package/src/components/form/FormControl.js +1 -0
  25. package/src/components/form/field/Field.js +1 -0
  26. package/src/components/form/field/types/StringField.js +1 -1
  27. package/src/components/icon/Icon.js +1 -0
  28. package/src/components/list/ListItem.js +1 -0
  29. package/src/components/menu/Menu.js +3 -0
  30. package/src/components/menu/MenuDivider.js +1 -0
  31. package/src/components/menu/MenuGroup.js +1 -0
  32. package/src/components/menu/MenuItem.js +1 -0
  33. package/src/components/menu/MenuLink.js +1 -0
  34. package/src/components/modal/Modal.js +4 -0
  35. package/src/components/pagination/Pagination.js +1 -0
  36. package/src/components/popover/Popover.js +1 -0
  37. package/src/components/popover/PopoverFooter.js +1 -0
  38. package/src/components/popover/PopoverHeader.js +1 -0
  39. package/src/components/progress/Progress.js +5 -0
  40. package/src/components/skeleton/Skeleton.js +4 -0
  41. package/src/components/slider/Slider.js +1 -0
  42. package/src/components/spacer/Spacer.js +1 -0
  43. package/src/components/spinner/Spinner.js +1 -0
  44. package/src/components/splitter/Splitter.js +1 -0
  45. package/src/components/splitter/SplitterGutter.js +6 -0
  46. package/src/components/splitter/SplitterPanel.js +1 -0
  47. package/src/components/stacks/AbsoluteStack.js +1 -0
  48. package/src/components/stacks/FixedStack.js +1 -0
  49. package/src/components/stacks/HStack.js +1 -0
  50. package/src/components/stacks/PositionStack.js +1 -0
  51. package/src/components/stacks/RelativeStack.js +1 -0
  52. package/src/components/stacks/Stack.js +1 -0
  53. package/src/components/stacks/VStack.js +1 -0
  54. package/src/components/stepper/Stepper.js +1 -0
  55. package/src/components/stepper/StepperStep.js +1 -0
  56. package/src/components/switch/Switch.js +1 -0
  57. package/src/components/table/DataTable.js +1 -0
  58. package/src/components/table/SimpleTable.js +1 -0
  59. package/src/components/tabs/Tabs.js +1 -0
  60. package/src/components/toast/Toast.js +1 -0
  61. package/src/components/tooltip/Tooltip.js +1 -0
  62. package/src/core/data/MemoryManager.js +6 -1
  63. package/src/core/data/ObservableItem.js +33 -15
  64. package/src/core/data/ObservableObject.js +1 -2
  65. package/src/core/elements/anchor/anchor.js +4 -3
  66. package/src/core/elements/anchor/one-child-anchor-overwriting.js +4 -4
  67. package/src/core/elements/content-formatter.js +19 -0
  68. package/src/core/elements/control/for-each-array-cache.js +59 -0
  69. package/src/core/elements/control/for-each-array.js +15 -23
  70. package/src/core/wrappers/AttributesWrapper.js +12 -12
  71. package/src/core/wrappers/ElementCreator.js +33 -10
  72. package/src/core/wrappers/HtmlElementWrapper.js +17 -13
  73. package/src/core/wrappers/NDElement.js +23 -8
  74. package/src/core/wrappers/NdPrototype.js +64 -39
  75. package/src/core/wrappers/constants.js +12 -12
  76. package/src/core/wrappers/prototypes/nd-element-extensions.js +127 -57
  77. package/src/core/wrappers/template-cloner/NodeCloner.js +10 -6
  78. package/src/core/wrappers/template-cloner/TemplateCloner.js +7 -4
  79. package/src/fetch/NativeFetch.js +1 -0
  80. package/src/ui/components/accordion/AccordionItemRender.js +29 -7
  81. package/src/ui/components/alert/AlertRender.js +6 -0
  82. package/src/ui/components/breadcrumb/BreadcrumbRender.js +9 -1
  83. package/src/ui/components/button/ButtonRender.js +3 -0
  84. package/src/ui/components/contextmenu/ContextmenuRender.js +2 -0
  85. package/src/ui/components/dropdown/DropdownRender.js +3 -0
  86. package/src/ui/components/dropdown/item/DropdownItemRender.js +6 -1
  87. package/src/ui/components/form/FieldCollectionRender.js +4 -0
  88. package/src/ui/components/form/fields/CheckboxFieldRender.js +4 -0
  89. package/src/ui/components/form/fields/CheckboxGroupFieldRender.js +4 -0
  90. package/src/ui/components/form/fields/FieldRender.js +1 -1
  91. package/src/ui/components/form/fields/RadioFieldRender.js +4 -0
  92. package/src/ui/components/form/fields/RangeFieldRender.js +4 -0
  93. package/src/ui/components/form/fields/SelectFieldRender.js +5 -0
  94. package/src/ui/components/form/fields/SliderFieldRender.js +3 -0
  95. package/src/ui/components/icon/tabler/TablerIconRender.js +0 -1
  96. package/src/ui/components/list/item/ListItemRender.js +4 -0
  97. package/src/ui/components/menu/MenuItemRender.js +8 -0
  98. package/src/ui/components/menu/MenuLinkRender.js +11 -0
  99. package/src/ui/components/menu/MenuRender.js +5 -0
  100. package/src/ui/components/menu/helpers.js +2 -1
  101. package/src/ui/components/modal/ModalRender.js +8 -2
  102. package/src/ui/components/progress/ProgressRender.js +9 -0
  103. package/src/ui/components/skeleton/SkeletonRender.js +2 -0
  104. package/src/ui/components/spinner/SpinnerRender.js +3 -0
  105. package/src/ui/components/splitter/SplitterGutterRender.js +4 -0
  106. package/src/ui/components/splitter/SplitterPanelRender.js +4 -0
  107. package/src/ui/components/splitter/SplitterRender.js +2 -0
  108. package/src/ui/components/stepper/StepperRender.js +2 -1
  109. package/src/ui/components/stepper/StepperStepRender.js +8 -1
  110. package/src/ui/components/switch/SwitchRender.js +3 -0
  111. package/src/ui/components/table/data-table/DataTableRender.js +4 -0
  112. package/src/ui/components/table/simple-table/SimpleTableRender.js +2 -0
  113. package/src/ui/components/toast/ToastRender.js +4 -0
  114. package/src/ui/utils/aria.js +19 -0
  115. package/tests/integration/ui/attributes-wrapper.test.js +177 -0
  116. package/tests/integration/ui/for-each-array.test.js +167 -0
  117. package/tests/integration/ui/lifecycle.test.js +146 -0
  118. package/tests/integration/ui/show-if.test.js +168 -0
  119. package/tests/unit/core/observable-array.test.js +404 -0
  120. package/tests/unit/core/observable-item.test.js +428 -0
  121. package/tests/unit/core/observable-object.test.js +239 -0
  122. package/tests/unit/core/observable-resource.test.js +323 -0
  123. package/tests/unit/core/store.test.js +347 -0
  124. package/tests/unit/router/route-matching.test.js +169 -0
  125. package/tests/unit/router/router-guards.test.js +189 -0
  126. package/tests/unit/utils/cache.test.js +120 -0
  127. package/tests/unit/utils/filters.test.js +394 -0
  128. package/types/elements.d.ts +5 -0
  129. package/types/native-fetch.d.ts +1 -1
  130. package/types/observable-resource.d.ts +2 -2
  131. package/types/observable.d.ts +137 -21
  132. package/types/service.d.ts +2 -1
  133. package/utils.js +7 -4
  134. package/vitest.config.js +15 -0
@@ -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
+ });
@@ -17,8 +17,12 @@ export default function NodeCloner($element) {
17
17
  this.$styles = null;
18
18
  this.$attrs = null;
19
19
  this.$ndMethods = null;
20
+ this.$content = null;
20
21
  }
21
22
 
23
+ NodeCloner.prototype.shouldBeHydrate = function() {
24
+ return this.$attrs !== null || this.$classes !== null || this.$styles !== null || this.$ndMethods !== null || this.$content !== null;
25
+ };
22
26
 
23
27
  /**
24
28
  * Attaches a template binding to the element by hydrating it with the specified method.
@@ -171,16 +175,16 @@ NodeCloner.prototype.attach = function(methodName, callback) {
171
175
  * Registers a reactive text content binding for the element.
172
176
  *
173
177
  * @internal
174
- * @param {Function} valueorProperty - Function receiving data and returning the text content
178
+ * @param {Function} valueOrProperty - Function receiving data and returning the text content
175
179
  * @returns {NodeCloner} this
176
180
  */
177
- NodeCloner.prototype.text = function(valueorProperty) {
178
- this.$content = valueorProperty;
179
- if(typeof valueorProperty === 'function') {
180
- this.cloneNode = (data) => createTextNode(valueorProperty.apply(null, data));
181
+ NodeCloner.prototype.text = function(valueOrProperty) {
182
+ this.$content = valueOrProperty;
183
+ if(typeof valueOrProperty === 'function') {
184
+ this.cloneNode = (data) => createTextNode(valueOrProperty.apply(null, data));
181
185
  return this;
182
186
  }
183
- this.cloneNode = (data) => createTextNode(data[0][valueorProperty]);
187
+ this.cloneNode = (data) => createTextNode(data[0][valueOrProperty]);
184
188
  return this;
185
189
  };
186
190
 
@@ -24,11 +24,11 @@ export function TemplateCloner($fn) {
24
24
 
25
25
  const assignClonerToNode = ($node) => {
26
26
  const childNodes = $node.childNodes;
27
- let containDynamicNode = !!$node.nodeCloner;
27
+ let containDynamicNode = $node.nodeCloner?.shouldBeHydrate();
28
28
  const childNodesLength = childNodes.length;
29
29
  for(let i = 0; i < childNodesLength; i++) {
30
30
  const child = childNodes[i];
31
- if(child.nodeCloner) {
31
+ if(child.nodeCloner && child.nodeCloner.shouldBeHydrate()) {
32
32
  containDynamicNode = true;
33
33
  }
34
34
  const localContainDynamicNode = assignClonerToNode(child);
@@ -179,9 +179,12 @@ export function useCache(fn) {
179
179
  return node;
180
180
  };
181
181
 
182
+ if(fn.length === 0) {
183
+ return () => wrapper();
184
+ }
182
185
  if(fn.length < 2) {
183
- return (...args) => {
184
- return wrapper(args);
186
+ return (arg) => {
187
+ return wrapper([arg]);
185
188
  };
186
189
  }
187
190
  return (_, __, ...args) => {
@@ -15,6 +15,7 @@ export default function NativeFetch($baseUrl) {
15
15
  };
16
16
 
17
17
  this.fetch = async function(method, endpoint, params = {}, options = {}) {
18
+ endpoint = endpoint || '';
18
19
  if(options.formData) {
19
20
  const formData = new FormData();
20
21
  for(const key in params) {
@@ -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();
@@ -25,6 +25,7 @@ export default function CheckboxFieldRender($desc, instance) {
25
25
  }
26
26
 
27
27
  const buildCheckbox = ($desc, instance) => {
28
+ const isRequired = $desc.rules?.some(rule => rule.fn?.name === 'required');
28
29
  const input = Input({
29
30
  class: 'field-checkbox',
30
31
  type: 'checkbox',
@@ -32,6 +33,9 @@ const buildCheckbox = ($desc, instance) => {
32
33
  id: $desc.id || $desc.name,
33
34
  disabled: $desc.disabled,
34
35
  checked: $desc.checked,
36
+ // [a11y] aria-required, aria-invalid
37
+ ...(isRequired ? { 'aria-required': 'true' } : {}),
38
+ ...($desc.hasErrors ? { 'aria-invalid': $desc.hasErrors } : {}),
35
39
  ...($desc.elementsProps.input || {}),
36
40
  });
37
41
 
@@ -85,8 +85,12 @@ const buildOptions = ($desc, instance) => {
85
85
  ]);
86
86
  };
87
87
 
88
+ const isRequired = $desc.rules?.some(rule => rule.fn?.name === 'required');
88
89
  return Div({
89
90
  class: 'field-checkbox-group is-' + ($desc.layout || 'vertical'),
91
+ // [a11y] role=group, aria-required
92
+ role: 'group',
93
+ ...( isRequired ? { 'aria-required': 'true' } : {} ),
90
94
  ...($desc.elementsProps.wrapper || {}),
91
95
  }, ForEachArray($desc.options, buildItem));
92
96
  };
@@ -110,7 +110,7 @@ const buildErrors = ($desc) => {
110
110
  return ShowIf($desc.hasErrors,
111
111
  () => Div({class: 'field-errors', ...($desc.elementsProps.error || {})},
112
112
  ForEachArray($desc.errors, (error) =>
113
- Span({class: 'field-error'}, error),
113
+ Span({class: 'field-error'}, (typeof error === 'string' ? error.resolve() : error)),
114
114
  ),
115
115
  ),
116
116
  );
@@ -70,8 +70,12 @@ const buildOptions = ($desc, instance) => {
70
70
  ]);
71
71
  };
72
72
 
73
+ const isRequired = $desc.rules?.some(rule => rule.fn?.name === 'required');
73
74
  return Div({
74
75
  class: 'field-radio-group is-' + ($desc.layout || 'vertical'),
76
+ // [a11y] role=radiogroup, aria-required
77
+ role: 'radiogroup',
78
+ ...( isRequired ? { 'aria-required': 'true' } : {} ),
75
79
  ...($desc.elementsProps.wrapper || {}),
76
80
  }, ForEachArray($options, buildOption));
77
81
  };
@@ -66,6 +66,10 @@ const buildInput = ($desc, instance) => {
66
66
  step: $desc.step ?? 1,
67
67
  disabled: $desc.disabled,
68
68
  value: $desc.value,
69
+ // [a11y] aria-valuemin, aria-valuemax, aria-valuenow
70
+ 'aria-valuemin': String($desc.min ?? 0),
71
+ 'aria-valuemax': String($desc.max ?? 100),
72
+ ...($desc.value?.__$Observable ? { 'aria-valuenow': $desc.value.transform((v) => String(v)) } : { 'aria-valuenow': String($desc.value ?? 0) }),
69
73
  ...($desc.elementsProps.input || {}),
70
74
  });
71
75
 
@@ -61,8 +61,13 @@ const buildSelectWrapper = ($desc, instance) => {
61
61
  }
62
62
  triggerOptions.push(Span({class: 'select-field-chevron'}, '▾'));
63
63
 
64
+ const isRequired = $desc.rules?.some(rule => rule.fn?.name === 'required');
64
65
  const trigger = Div({
65
66
  class: {'field-input select-field-trigger': true, 'has-tags': isTags},
67
+ // [a11y] aria-haspopup, aria-expanded, aria-required
68
+ 'aria-haspopup': 'listbox',
69
+ 'aria-expanded': 'false',
70
+ ...( isRequired ? { 'aria-required': 'true' } : {} ),
66
71
  ...($desc.elementsProps.input || {}),
67
72
  }, [
68
73
  valueDisplay,
@@ -7,6 +7,9 @@ export default function SliderRender($desc, instance) {
7
7
  const props = instance.getEditableProps();
8
8
 
9
9
  props.class.add('slider');
10
+ // [a11y] aria-valuemin, aria-valuemax
11
+ props['aria-valuemin'] = String($desc.min ?? 0);
12
+ props['aria-valuemax'] = String($desc.max ?? 100);
10
13
  props.class.add({
11
14
  'is-vertical': $desc.vertical,
12
15
  'is-reverse': $desc.reverse,
@@ -49,7 +49,6 @@ export default function TablerIconRender($desc) {
49
49
  if($desc.color) {
50
50
  style.color = $desc.color;
51
51
  }
52
- console.log(style);
53
52
 
54
53
  return Italic({
55
54
  'aria-hidden': 'true',
@@ -13,9 +13,13 @@ export default function ListItemRender($desc, instance) {
13
13
 
14
14
  if ($desc.disabled) {
15
15
  props.class.add('is-disabled', $desc.disabled);
16
+ // [a11y] aria-disabled
17
+ props['aria-disabled'] = $desc.disabled;
16
18
  }
17
19
  if ($desc.selected) {
18
20
  props.class.add('is-selected', $desc.selected);
21
+ // [a11y] aria-selected
22
+ props['aria-selected'] = $desc.selected;
19
23
  }
20
24
  if ($desc.visibility) {
21
25
  props.class.add('is-hidden', $desc.visibility.isFalsy());
@@ -24,6 +24,14 @@ export default function MenuItemRender($desc, instance) {
24
24
 
25
25
  if(hasSubmenu) {
26
26
  props.class.add('has-submenu');
27
+ // [a11y] aria-haspopup for submenu items
28
+ props['aria-haspopup'] = 'menu';
29
+ props['aria-expanded'] = 'false';
30
+ }
31
+
32
+ // [a11y] aria-disabled
33
+ if($desc.disabled) {
34
+ props['aria-disabled'] = $desc.disabled;
27
35
  }
28
36
 
29
37
  const content = [