native-document 1.0.168 → 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 (153) hide show
  1. package/CHANGELOG.md +99 -4
  2. package/components.js +3 -1
  3. package/dist/native-document.components.min.js +197 -171
  4. package/dist/native-document.dev.js +92 -75
  5. package/dist/native-document.dev.js.map +1 -1
  6. package/dist/native-document.min.js +1 -1
  7. package/docs/components/icons.md +321 -0
  8. package/docs/vitepress-conventions.md +2 -2
  9. package/package.json +11 -4
  10. package/src/components/BaseComponent.js +43 -1
  11. package/src/components/accordion/Accordion.js +1 -0
  12. package/src/components/accordion/AccordionItem.js +1 -0
  13. package/src/components/alert/Alert.js +4 -0
  14. package/src/components/avatar/Avatar.js +1 -0
  15. package/src/components/avatar/AvatarGroup.js +1 -0
  16. package/src/components/badge/Badge.js +1 -0
  17. package/src/components/breadcrumb/BreadCrumb.js +4 -0
  18. package/src/components/button/Button.js +1 -0
  19. package/src/components/card/Card.js +1 -0
  20. package/src/components/context-menu/ContextMenu.js +2 -0
  21. package/src/components/context-menu/ContextMenuGroup.js +2 -0
  22. package/src/components/context-menu/ContextMenuItem.js +2 -0
  23. package/src/components/divider/Divider.js +2 -0
  24. package/src/components/dropdown/Dropdown.js +1 -0
  25. package/src/components/dropdown/DropdownDivider.js +1 -0
  26. package/src/components/dropdown/DropdownGroup.js +1 -0
  27. package/src/components/dropdown/DropdownItem.js +1 -0
  28. package/src/components/dropdown/DropdownTrigger.js +1 -0
  29. package/src/components/form/FormControl.js +1 -0
  30. package/src/components/form/field/Field.js +1 -0
  31. package/src/components/form/field/types/FileField.js +3 -3
  32. package/src/components/form/field/types/StringField.js +1 -1
  33. package/src/components/icon/Icon.js +108 -0
  34. package/src/components/icon/icon-getters.js +142 -0
  35. package/src/components/icon/icons.js +171 -0
  36. package/src/components/icon/index.js +17 -0
  37. package/src/components/icon/types/Icon.d.ts +191 -0
  38. package/src/components/index.d.ts +6 -1
  39. package/src/components/list/ListItem.js +1 -0
  40. package/src/components/list/types/List.d.ts +45 -32
  41. package/src/components/list/types/ListDivider.ts +16 -0
  42. package/src/components/list/types/ListGroup.d.ts +51 -29
  43. package/src/components/list/types/ListItem.d.ts +20 -21
  44. package/src/components/menu/Menu.js +3 -0
  45. package/src/components/menu/MenuDivider.js +1 -0
  46. package/src/components/menu/MenuGroup.js +1 -0
  47. package/src/components/menu/MenuItem.js +1 -0
  48. package/src/components/menu/MenuLink.js +1 -0
  49. package/src/components/modal/Modal.js +4 -0
  50. package/src/components/pagination/Pagination.js +1 -0
  51. package/src/components/popover/Popover.js +1 -0
  52. package/src/components/popover/PopoverFooter.js +1 -0
  53. package/src/components/popover/PopoverHeader.js +1 -0
  54. package/src/components/progress/Progress.js +5 -0
  55. package/src/components/skeleton/Skeleton.js +4 -0
  56. package/src/components/slider/Slider.js +1 -0
  57. package/src/components/spacer/Spacer.js +1 -0
  58. package/src/components/spinner/Spinner.js +1 -0
  59. package/src/components/splitter/Splitter.js +1 -0
  60. package/src/components/splitter/SplitterGutter.js +6 -0
  61. package/src/components/splitter/SplitterPanel.js +1 -0
  62. package/src/components/stacks/AbsoluteStack.js +1 -0
  63. package/src/components/stacks/FixedStack.js +1 -0
  64. package/src/components/stacks/HStack.js +1 -0
  65. package/src/components/stacks/PositionStack.js +1 -0
  66. package/src/components/stacks/RelativeStack.js +1 -0
  67. package/src/components/stacks/Stack.js +1 -0
  68. package/src/components/stacks/VStack.js +1 -0
  69. package/src/components/stepper/Stepper.js +1 -0
  70. package/src/components/stepper/StepperStep.js +1 -0
  71. package/src/components/switch/Switch.js +1 -0
  72. package/src/components/table/DataTable.js +1 -0
  73. package/src/components/table/SimpleTable.js +1 -0
  74. package/src/components/tabs/Tabs.js +1 -0
  75. package/src/components/toast/Toast.js +1 -0
  76. package/src/components/tooltip/Tooltip.js +1 -0
  77. package/src/core/data/MemoryManager.js +6 -1
  78. package/src/core/data/ObservableItem.js +28 -10
  79. package/src/core/elements/anchor/anchor.js +4 -3
  80. package/src/core/elements/anchor/one-child-anchor-overwriting.js +4 -4
  81. package/src/core/elements/content-formatter.js +35 -2
  82. package/src/core/elements/control/for-each-array-cache.js +59 -0
  83. package/src/core/elements/control/for-each-array.js +15 -23
  84. package/src/core/elements/form.js +1 -1
  85. package/src/core/elements/img.js +1 -1
  86. package/src/core/elements/medias.js +2 -2
  87. package/src/core/elements/meta-data.js +1 -1
  88. package/src/core/wrappers/AttributesWrapper.js +44 -29
  89. package/src/core/wrappers/ElementCreator.js +38 -22
  90. package/src/core/wrappers/HtmlElementWrapper.js +33 -9
  91. package/src/core/wrappers/NDElement.js +35 -9
  92. package/src/core/wrappers/NdPrototype.js +31 -39
  93. package/src/core/wrappers/constants.js +12 -12
  94. package/src/core/wrappers/prototypes/attributes-extensions.js +24 -24
  95. package/src/core/wrappers/prototypes/nd-element-extensions.js +128 -65
  96. package/src/ui/components/accordion/AccordionItemRender.js +29 -7
  97. package/src/ui/components/alert/AlertRender.js +6 -0
  98. package/src/ui/components/breadcrumb/BreadcrumbRender.js +9 -1
  99. package/src/ui/components/button/ButtonRender.js +3 -0
  100. package/src/ui/components/contextmenu/ContextmenuRender.js +2 -0
  101. package/src/ui/components/dropdown/DropdownRender.js +3 -0
  102. package/src/ui/components/dropdown/item/DropdownItemRender.js +6 -1
  103. package/src/ui/components/form/FieldCollectionRender.js +4 -0
  104. package/src/ui/components/form/fields/CheckboxFieldRender.js +4 -0
  105. package/src/ui/components/form/fields/CheckboxGroupFieldRender.js +4 -0
  106. package/src/ui/components/form/fields/RadioFieldRender.js +4 -0
  107. package/src/ui/components/form/fields/RangeFieldRender.js +4 -0
  108. package/src/ui/components/form/fields/SelectFieldRender.js +5 -0
  109. package/src/ui/components/form/fields/SliderFieldRender.js +3 -0
  110. package/src/ui/components/icon/material/MaterialIconRender.js +71 -0
  111. package/src/ui/components/icon/material/material.css +15 -0
  112. package/src/ui/components/icon/material/material.map.js +170 -0
  113. package/src/ui/components/icon/phosphor/PhosphorIconRender.js +71 -0
  114. package/src/ui/components/icon/phosphor/phosphor.css +17 -0
  115. package/src/ui/components/icon/phosphor/phosphor.map.js +165 -0
  116. package/src/ui/components/icon/tabler/TablerIconRender.js +58 -0
  117. package/src/ui/components/icon/tabler/tabler.css +14 -0
  118. package/src/ui/components/icon/tabler/tabler.map.js +165 -0
  119. package/src/ui/components/list/item/ListItemRender.js +4 -0
  120. package/src/ui/components/menu/MenuItemRender.js +8 -0
  121. package/src/ui/components/menu/MenuLinkRender.js +11 -0
  122. package/src/ui/components/menu/MenuRender.js +5 -0
  123. package/src/ui/components/menu/helpers.js +2 -1
  124. package/src/ui/components/modal/ModalRender.js +8 -2
  125. package/src/ui/components/progress/ProgressRender.js +9 -0
  126. package/src/ui/components/skeleton/SkeletonRender.js +2 -0
  127. package/src/ui/components/spinner/SpinnerRender.js +3 -0
  128. package/src/ui/components/splitter/SplitterGutterRender.js +4 -0
  129. package/src/ui/components/splitter/SplitterPanelRender.js +4 -0
  130. package/src/ui/components/splitter/SplitterRender.js +2 -0
  131. package/src/ui/components/stepper/StepperRender.js +2 -1
  132. package/src/ui/components/stepper/StepperStepRender.js +8 -1
  133. package/src/ui/components/switch/SwitchRender.js +3 -0
  134. package/src/ui/components/table/data-table/DataTableRender.js +4 -0
  135. package/src/ui/components/table/simple-table/SimpleTableRender.js +2 -0
  136. package/src/ui/components/toast/ToastRender.js +4 -0
  137. package/src/ui/index.js +5 -0
  138. package/src/ui/utils/aria.js +19 -0
  139. package/tests/integration/ui/attributes-wrapper.test.js +177 -0
  140. package/tests/integration/ui/for-each-array.test.js +167 -0
  141. package/tests/integration/ui/lifecycle.test.js +146 -0
  142. package/tests/integration/ui/show-if.test.js +168 -0
  143. package/tests/unit/core/observable-array.test.js +404 -0
  144. package/tests/unit/core/observable-item.test.js +428 -0
  145. package/tests/unit/core/observable-object.test.js +239 -0
  146. package/tests/unit/core/observable-resource.test.js +323 -0
  147. package/tests/unit/core/store.test.js +347 -0
  148. package/tests/unit/router/route-matching.test.js +169 -0
  149. package/tests/unit/router/router-guards.test.js +189 -0
  150. package/tests/unit/utils/cache.test.js +120 -0
  151. package/tests/unit/utils/filters.test.js +394 -0
  152. package/types/elements.d.ts +5 -0
  153. package/vitest.config.js +15 -0
@@ -1,110 +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 () {
91
- const element = this.$element ?? this.$build?.() ?? this.build?.() ?? null;
92
- if(this.$attachements) {
93
- if(!this.$attachements.contains(this.$element)) {
94
- this.$attachements.append(this.$element);
95
- }
96
- return this.$attachements;
97
- }
98
- return element;
99
- };
162
+ defineToNdElement(NDElement.prototype, function () {
163
+ return this.$element;
164
+ });
100
165
 
101
166
  /**
102
- * Converts the array to a DocumentFragment containing all elements.
103
- * Each item is processed through ElementCreator.getChild().
167
+ * Converts the array to a DocumentFragment containing all child elements.
104
168
  *
105
- * @returns {DocumentFragment} Fragment containing all array children
169
+ * @returns {DocumentFragment}
106
170
  */
107
- Array.prototype.toNdElement = function () {
171
+ defineToNdElement(Array.prototype, function () {
108
172
  const fragment = document.createDocumentFragment();
109
173
  for(let i = 0, length = this.length; i < length; i++) {
110
174
  const child = ElementCreator.getChild(this[i]);
@@ -112,27 +176,26 @@ Array.prototype.toNdElement = function () {
112
176
  fragment.appendChild(child);
113
177
  }
114
178
  return fragment;
115
- };
179
+ });
116
180
 
117
181
  /**
118
182
  * Calls the function and converts its return value to a DOM node.
119
- * Used internally by ElementCreator to process function-based children.
120
183
  *
121
- * @returns {Node} The DOM node returned by the function
184
+ * @returns {Node}
122
185
  */
123
- Function.prototype.toNdElement = function () {
186
+ defineToNdElement(Function.prototype, function () {
124
187
  const child = this;
125
188
  if(process.env.NODE_ENV === 'development') {
126
189
  PluginsManager.emit('BeforeProcessComponent', child);
127
190
  }
128
191
  return ElementCreator.getChild(child());
129
- };
192
+ });
130
193
 
131
194
  /**
132
- * Converts the TemplateBinding to a hydratable DOM node for use in TemplateCloner.
195
+ * Converts the TemplateBinding to a hydratable DOM node.
133
196
  *
134
- * @returns {Node} Hydratable node
197
+ * @returns {Node}
135
198
  */
136
- TemplateBinding.prototype.toNdElement = function () {
199
+ defineToNdElement(TemplateBinding.prototype, function () {
137
200
  return ElementCreator.createHydratableNode(null, this);
138
- };
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();
@@ -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
  };
@@ -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,
@@ -0,0 +1,71 @@
1
+ import './material.css';
2
+ import MAP from './material.map.js';
3
+ import { Span } from '../../../../core/elements';
4
+
5
+ const SIZES = {
6
+ small: 16,
7
+ medium: 24,
8
+ large: 32,
9
+ extraLarge: 48,
10
+ };
11
+
12
+ // Variant -> CSS class
13
+ // Material Icons uses the class as the font-family selector
14
+ const VARIANT_CLASS = {
15
+ fill: 'material-icons',
16
+ regular: 'material-icons',
17
+ outline: 'material-icons-outlined',
18
+ round: 'material-icons-round',
19
+ sharp: 'material-icons-sharp',
20
+ twoTone: 'material-icons-two-tone',
21
+ };
22
+
23
+ /**
24
+ * Renders a Google Material Icon as a <span> element using CSS webfont ligatures.
25
+ * The icon name is the text content of the element — Material Icons uses ligatures.
26
+ *
27
+ * Variants: fill/regular (default), outline, round, sharp, twoTone
28
+ * Each variant requires its own CSS - see material.css.
29
+ *
30
+ * @param {object} $desc
31
+ * @param {string} $desc.name
32
+ * @param {'fill'|'regular'|'outline'|'round'|'sharp'|'twoTone'|Observable<string>|null} $desc.variant
33
+ * @param {'small'|'medium'|'large'|'extraLarge'|number|Observable<string|number>|null} $desc.size
34
+ * @param {string|Observable<string>|null} $desc.color
35
+ * @returns {HTMLElement}
36
+ */
37
+ export default function MaterialIconRender($desc) {
38
+ const materialName = MAP[$desc.name] ?? $desc.name;
39
+ const style = {};
40
+
41
+ let className;
42
+
43
+ if($desc.variant?.__$Observable) {
44
+ className = $desc.variant.transform((variant) => {
45
+ return VARIANT_CLASS[variant] ?? VARIANT_CLASS.fill;
46
+ });
47
+ } else {
48
+ className = VARIANT_CLASS[$desc.variant] ?? VARIANT_CLASS.fill;
49
+ }
50
+
51
+ if($desc.size?.__$Observable) {
52
+ style.fontSize = $desc.size.transform((size) => {
53
+ const px = typeof size === 'number' ? size : (SIZES[size] ?? SIZES.medium);
54
+ return `${px}px`;
55
+ });
56
+ } else {
57
+ const px = typeof $desc.size === 'number' ? $desc.size : (SIZES[$desc.size] ?? SIZES.medium);
58
+ style.fontSize = `${px}px`;
59
+ }
60
+
61
+ if($desc.color) {
62
+ style.color = $desc.color;
63
+ }
64
+
65
+ // Material Icons uses ligatures — the icon name is the text content
66
+ return Span({
67
+ 'aria-hidden': 'true',
68
+ class: className,
69
+ style,
70
+ }, materialName);
71
+ }
@@ -0,0 +1,15 @@
1
+ /*
2
+ * Google Material Icons webfont
3
+ * Install : npm install material-icons
4
+ *
5
+ * Import only the variants you need.
6
+ */
7
+ /*@import 'material-icons/iconfont/filled.css';*/
8
+
9
+ /*
10
+ * Uncomment the variants you need:
11
+ */
12
+ /* @import 'material-icons/iconfont/outlined.css'; */
13
+ /* @import 'material-icons/iconfont/round.css'; */
14
+ /* @import 'material-icons/iconfont/sharp.css'; */
15
+ /* @import 'material-icons/iconfont/two-tone.css'; */