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.
- package/CHANGELOG.md +100 -2
- package/docs/vitepress-conventions.md +2 -2
- package/index.d.ts +1 -1
- package/package.json +11 -4
- package/src/components/BaseComponent.js +43 -1
- package/src/components/accordion/Accordion.js +1 -0
- package/src/components/accordion/AccordionItem.js +1 -0
- package/src/components/alert/Alert.js +4 -0
- package/src/components/avatar/Avatar.js +1 -0
- package/src/components/avatar/AvatarGroup.js +1 -0
- package/src/components/badge/Badge.js +1 -0
- package/src/components/breadcrumb/BreadCrumb.js +4 -0
- package/src/components/button/Button.js +1 -0
- package/src/components/card/Card.js +1 -0
- package/src/components/context-menu/ContextMenu.js +2 -0
- package/src/components/context-menu/ContextMenuGroup.js +2 -0
- package/src/components/context-menu/ContextMenuItem.js +2 -0
- package/src/components/divider/Divider.js +2 -0
- package/src/components/dropdown/Dropdown.js +1 -0
- package/src/components/dropdown/DropdownDivider.js +1 -0
- package/src/components/dropdown/DropdownGroup.js +1 -0
- package/src/components/dropdown/DropdownItem.js +1 -0
- package/src/components/dropdown/DropdownTrigger.js +1 -0
- package/src/components/form/FormControl.js +1 -0
- package/src/components/form/field/Field.js +1 -0
- package/src/components/form/field/types/StringField.js +1 -1
- package/src/components/icon/Icon.js +1 -0
- package/src/components/list/ListItem.js +1 -0
- package/src/components/menu/Menu.js +3 -0
- package/src/components/menu/MenuDivider.js +1 -0
- package/src/components/menu/MenuGroup.js +1 -0
- package/src/components/menu/MenuItem.js +1 -0
- package/src/components/menu/MenuLink.js +1 -0
- package/src/components/modal/Modal.js +4 -0
- package/src/components/pagination/Pagination.js +1 -0
- package/src/components/popover/Popover.js +1 -0
- package/src/components/popover/PopoverFooter.js +1 -0
- package/src/components/popover/PopoverHeader.js +1 -0
- package/src/components/progress/Progress.js +5 -0
- package/src/components/skeleton/Skeleton.js +4 -0
- package/src/components/slider/Slider.js +1 -0
- package/src/components/spacer/Spacer.js +1 -0
- package/src/components/spinner/Spinner.js +1 -0
- package/src/components/splitter/Splitter.js +1 -0
- package/src/components/splitter/SplitterGutter.js +6 -0
- package/src/components/splitter/SplitterPanel.js +1 -0
- package/src/components/stacks/AbsoluteStack.js +1 -0
- package/src/components/stacks/FixedStack.js +1 -0
- package/src/components/stacks/HStack.js +1 -0
- package/src/components/stacks/PositionStack.js +1 -0
- package/src/components/stacks/RelativeStack.js +1 -0
- package/src/components/stacks/Stack.js +1 -0
- package/src/components/stacks/VStack.js +1 -0
- package/src/components/stepper/Stepper.js +1 -0
- package/src/components/stepper/StepperStep.js +1 -0
- package/src/components/switch/Switch.js +1 -0
- package/src/components/table/DataTable.js +1 -0
- package/src/components/table/SimpleTable.js +1 -0
- package/src/components/tabs/Tabs.js +1 -0
- package/src/components/toast/Toast.js +1 -0
- package/src/components/tooltip/Tooltip.js +1 -0
- package/src/core/data/MemoryManager.js +6 -1
- package/src/core/data/ObservableItem.js +33 -15
- package/src/core/data/ObservableObject.js +1 -2
- package/src/core/elements/anchor/anchor.js +4 -3
- package/src/core/elements/anchor/one-child-anchor-overwriting.js +4 -4
- package/src/core/elements/content-formatter.js +19 -0
- package/src/core/elements/control/for-each-array-cache.js +59 -0
- package/src/core/elements/control/for-each-array.js +15 -23
- package/src/core/wrappers/AttributesWrapper.js +12 -12
- package/src/core/wrappers/ElementCreator.js +33 -10
- package/src/core/wrappers/HtmlElementWrapper.js +17 -13
- package/src/core/wrappers/NDElement.js +23 -8
- package/src/core/wrappers/NdPrototype.js +64 -39
- package/src/core/wrappers/constants.js +12 -12
- package/src/core/wrappers/prototypes/nd-element-extensions.js +127 -57
- package/src/core/wrappers/template-cloner/NodeCloner.js +10 -6
- package/src/core/wrappers/template-cloner/TemplateCloner.js +7 -4
- package/src/fetch/NativeFetch.js +1 -0
- package/src/ui/components/accordion/AccordionItemRender.js +29 -7
- package/src/ui/components/alert/AlertRender.js +6 -0
- package/src/ui/components/breadcrumb/BreadcrumbRender.js +9 -1
- package/src/ui/components/button/ButtonRender.js +3 -0
- package/src/ui/components/contextmenu/ContextmenuRender.js +2 -0
- package/src/ui/components/dropdown/DropdownRender.js +3 -0
- package/src/ui/components/dropdown/item/DropdownItemRender.js +6 -1
- package/src/ui/components/form/FieldCollectionRender.js +4 -0
- package/src/ui/components/form/fields/CheckboxFieldRender.js +4 -0
- package/src/ui/components/form/fields/CheckboxGroupFieldRender.js +4 -0
- package/src/ui/components/form/fields/FieldRender.js +1 -1
- package/src/ui/components/form/fields/RadioFieldRender.js +4 -0
- package/src/ui/components/form/fields/RangeFieldRender.js +4 -0
- package/src/ui/components/form/fields/SelectFieldRender.js +5 -0
- package/src/ui/components/form/fields/SliderFieldRender.js +3 -0
- package/src/ui/components/icon/tabler/TablerIconRender.js +0 -1
- package/src/ui/components/list/item/ListItemRender.js +4 -0
- package/src/ui/components/menu/MenuItemRender.js +8 -0
- package/src/ui/components/menu/MenuLinkRender.js +11 -0
- package/src/ui/components/menu/MenuRender.js +5 -0
- package/src/ui/components/menu/helpers.js +2 -1
- package/src/ui/components/modal/ModalRender.js +8 -2
- package/src/ui/components/progress/ProgressRender.js +9 -0
- package/src/ui/components/skeleton/SkeletonRender.js +2 -0
- package/src/ui/components/spinner/SpinnerRender.js +3 -0
- package/src/ui/components/splitter/SplitterGutterRender.js +4 -0
- package/src/ui/components/splitter/SplitterPanelRender.js +4 -0
- package/src/ui/components/splitter/SplitterRender.js +2 -0
- package/src/ui/components/stepper/StepperRender.js +2 -1
- package/src/ui/components/stepper/StepperStepRender.js +8 -1
- package/src/ui/components/switch/SwitchRender.js +3 -0
- package/src/ui/components/table/data-table/DataTableRender.js +4 -0
- package/src/ui/components/table/simple-table/SimpleTableRender.js +2 -0
- package/src/ui/components/toast/ToastRender.js +4 -0
- package/src/ui/utils/aria.js +19 -0
- package/tests/integration/ui/attributes-wrapper.test.js +177 -0
- package/tests/integration/ui/for-each-array.test.js +167 -0
- package/tests/integration/ui/lifecycle.test.js +146 -0
- package/tests/integration/ui/show-if.test.js +168 -0
- package/tests/unit/core/observable-array.test.js +404 -0
- package/tests/unit/core/observable-item.test.js +428 -0
- package/tests/unit/core/observable-object.test.js +239 -0
- package/tests/unit/core/observable-resource.test.js +323 -0
- package/tests/unit/core/store.test.js +347 -0
- package/tests/unit/router/route-matching.test.js +169 -0
- package/tests/unit/router/router-guards.test.js +189 -0
- package/tests/unit/utils/cache.test.js +120 -0
- package/tests/unit/utils/filters.test.js +394 -0
- package/types/elements.d.ts +5 -0
- package/types/native-fetch.d.ts +1 -1
- package/types/observable-resource.d.ts +2 -2
- package/types/observable.d.ts +137 -21
- package/types/service.d.ts +2 -1
- package/utils.js +7 -4
- 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
|
-
|
|
10
|
+
|
|
11
|
+
const OBS_PATTERN = /({{obs:\d+}})/;
|
|
12
|
+
const OBS_ID_PATTERN = /\d+/;
|
|
13
|
+
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
|
-
*
|
|
16
|
+
* Parses a string containing Observable placeholders and returns an array
|
|
17
|
+
* of strings and Observable instances.
|
|
13
18
|
*
|
|
14
|
-
*
|
|
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
|
|
17
|
-
|
|
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
|
-
*
|
|
46
|
+
* Registers a non-enumerable toNdElement method on a prototype.
|
|
22
47
|
*
|
|
23
|
-
* @
|
|
48
|
+
* @param {object} target - The prototype to extend
|
|
49
|
+
* @param {Function} fn - The toNdElement implementation
|
|
24
50
|
*/
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
*
|
|
61
|
+
* Marks a prototype as a valid NativeDocument child node.
|
|
31
62
|
*
|
|
32
|
-
* @
|
|
63
|
+
* @param {object} target - The prototype to mark
|
|
33
64
|
*/
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
*
|
|
75
|
+
* Marks a prototype as a native DOM node.
|
|
76
|
+
* Faster alternative to instanceof Node for hot paths.
|
|
40
77
|
*
|
|
41
|
-
* @
|
|
78
|
+
* @param {object} target - The prototype to mark
|
|
42
79
|
*/
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
*
|
|
118
|
+
* Converts a string to a static text node.
|
|
49
119
|
*
|
|
50
|
-
* @returns {
|
|
120
|
+
* @returns {Text}
|
|
51
121
|
*/
|
|
52
|
-
|
|
53
|
-
return this;
|
|
54
|
-
};
|
|
122
|
+
defineToNdElement(String.prototype, function () {
|
|
123
|
+
return ElementCreator.createStaticTextNodeWithoutParent(this);
|
|
124
|
+
});
|
|
55
125
|
|
|
56
126
|
/**
|
|
57
|
-
*
|
|
127
|
+
* Converts a number to a static text node.
|
|
58
128
|
*
|
|
59
|
-
* @returns {
|
|
129
|
+
* @returns {Text}
|
|
60
130
|
*/
|
|
61
|
-
|
|
62
|
-
return this;
|
|
63
|
-
};
|
|
131
|
+
defineToNdElement(Number.prototype, function () {
|
|
132
|
+
return ElementCreator.createStaticTextNodeWithoutParent(this.toString());
|
|
133
|
+
});
|
|
64
134
|
|
|
65
135
|
/**
|
|
66
|
-
* Returns the
|
|
136
|
+
* Returns the DOM node itself (identity).
|
|
67
137
|
*
|
|
68
|
-
* @returns {DocumentFragment}
|
|
138
|
+
* @returns {Element|Text|Comment|Document|DocumentFragment}
|
|
69
139
|
*/
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
147
|
+
* Converts an ObservableItem to a reactive text node that updates when the value changes.
|
|
76
148
|
*
|
|
77
|
-
* @returns {Text}
|
|
149
|
+
* @returns {Text}
|
|
78
150
|
*/
|
|
79
|
-
ObservableItem.prototype
|
|
80
|
-
return ElementCreator.
|
|
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
|
-
*
|
|
158
|
+
* Returns the underlying HTMLElement of this NDElement.
|
|
87
159
|
*
|
|
88
|
-
* @returns {HTMLElement|DocumentFragment}
|
|
160
|
+
* @returns {HTMLElement|DocumentFragment}
|
|
89
161
|
*/
|
|
90
|
-
NDElement.prototype
|
|
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}
|
|
169
|
+
* @returns {DocumentFragment}
|
|
99
170
|
*/
|
|
100
|
-
Array.prototype
|
|
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}
|
|
184
|
+
* @returns {Node}
|
|
115
185
|
*/
|
|
116
|
-
Function.prototype
|
|
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
|
|
195
|
+
* Converts the TemplateBinding to a hydratable DOM node.
|
|
126
196
|
*
|
|
127
|
-
* @returns {Node}
|
|
197
|
+
* @returns {Node}
|
|
128
198
|
*/
|
|
129
|
-
TemplateBinding.prototype
|
|
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}
|
|
178
|
+
* @param {Function} valueOrProperty - Function receiving data and returning the text content
|
|
175
179
|
* @returns {NodeCloner} this
|
|
176
180
|
*/
|
|
177
|
-
NodeCloner.prototype.text = function(
|
|
178
|
-
this.$content =
|
|
179
|
-
if(typeof
|
|
180
|
-
this.cloneNode = (data) => createTextNode(
|
|
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][
|
|
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 =
|
|
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 (
|
|
184
|
-
return wrapper(
|
|
186
|
+
return (arg) => {
|
|
187
|
+
return wrapper([arg]);
|
|
185
188
|
};
|
|
186
189
|
}
|
|
187
190
|
return (_, __, ...args) => {
|
package/src/fetch/NativeFetch.js
CHANGED
|
@@ -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
|
-
|
|
10
|
-
const
|
|
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({
|
|
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(
|
|
78
|
+
Div(panelProps, $desc.renderContent($desc, instance)),
|
|
57
79
|
);
|
|
58
80
|
}
|
|
59
81
|
|
|
60
82
|
return ShowIf($desc.expanded, () =>
|
|
61
|
-
Div(
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -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 = [
|