native-document 1.0.169 → 1.0.172
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -1
- package/docs/vitepress-conventions.md +2 -2
- 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 +28 -10
- 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 +9 -4
- package/src/core/wrappers/NDElement.js +23 -8
- package/src/core/wrappers/NdPrototype.js +31 -39
- package/src/core/wrappers/constants.js +12 -12
- package/src/core/wrappers/prototypes/nd-element-extensions.js +127 -57
- 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/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/vitest.config.js +15 -0
|
@@ -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,
|
|
@@ -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 = [
|
|
@@ -24,6 +24,17 @@ export default function MenuLinkRender($desc, instance) {
|
|
|
24
24
|
|
|
25
25
|
if(hasSubmenu) {
|
|
26
26
|
props.class.add('has-submenu');
|
|
27
|
+
// [a11y] aria-haspopup for submenu links
|
|
28
|
+
props['aria-haspopup'] = 'menu';
|
|
29
|
+
props['aria-expanded'] = 'false';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// [a11y] aria-disabled, aria-current
|
|
33
|
+
if($desc.disabled) {
|
|
34
|
+
props['aria-disabled'] = $desc.disabled;
|
|
35
|
+
}
|
|
36
|
+
if($desc.selected) {
|
|
37
|
+
props['aria-current'] = $desc.selected.transform ? $desc.selected.transform((s) => s ? 'page' : undefined) : ($desc.selected ? 'page' : undefined);
|
|
27
38
|
}
|
|
28
39
|
|
|
29
40
|
const content = [
|
|
@@ -8,6 +8,11 @@ export default function MenuRender($desc, instance) {
|
|
|
8
8
|
props.class.add('menu');
|
|
9
9
|
props.class.add('is-' + ($desc.orientation || 'horizontal'));
|
|
10
10
|
|
|
11
|
+
// [a11y] aria-label if provided
|
|
12
|
+
if($desc.label) {
|
|
13
|
+
props['aria-label'] = $desc.label;
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
const nav = Nav(instance.resolveProps(), ForEachArray($desc.items));
|
|
12
17
|
|
|
13
18
|
if($desc.compactThreshold) {
|
|
@@ -113,9 +113,10 @@ export const setupInteraction = (listItem, el, submenuEl, orientation, interacti
|
|
|
113
113
|
});
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
-
document.addEventListener('click', (
|
|
116
|
+
document.addEventListener('click', () => {
|
|
117
117
|
$isMenuActivated?.set(false);
|
|
118
118
|
$isOpen.set(false);
|
|
119
119
|
$activeItem?.set(null);
|
|
120
120
|
});
|
|
121
|
+
|
|
121
122
|
};
|
|
@@ -5,6 +5,7 @@ import {createPortal} from '../../../core/elements/anchor/anchor';
|
|
|
5
5
|
|
|
6
6
|
import './modal.css';
|
|
7
7
|
|
|
8
|
+
let _modalIdCounter = 0;
|
|
8
9
|
export default function ModalRender($desc, instance) {
|
|
9
10
|
const editableProps = instance.getEditableProps();
|
|
10
11
|
|
|
@@ -66,10 +67,15 @@ export default function ModalRender($desc, instance) {
|
|
|
66
67
|
return createPortal(dialog);
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
|
|
70
|
+
|
|
71
|
+
const buildHeader = ($desc, instance, dialog) => {
|
|
70
72
|
const content = [];
|
|
71
73
|
|
|
72
|
-
|
|
74
|
+
if($desc.title) {
|
|
75
|
+
const titleId = `modal-title-${++_modalIdCounter}`;
|
|
76
|
+
dialog.setAttribute('aria-labelledby', titleId);
|
|
77
|
+
content.push(Div({class: 'modal-title', id: titleId }, $desc.title));
|
|
78
|
+
}
|
|
73
79
|
|
|
74
80
|
if($desc.closable) {
|
|
75
81
|
content.push(
|
|
@@ -46,6 +46,15 @@ const buildBar = ($desc, instance) => {
|
|
|
46
46
|
|
|
47
47
|
props.class.add('progress');
|
|
48
48
|
|
|
49
|
+
// [a11y] aria-valuenow and aria-valuemax
|
|
50
|
+
const ariaMax = $desc.max || 100;
|
|
51
|
+
props['aria-valuemax'] = String(ariaMax);
|
|
52
|
+
if($desc.value?.__$Observable) {
|
|
53
|
+
props['aria-valuenow'] = $desc.value.transform((v) => String(Math.min(v, ariaMax)));
|
|
54
|
+
} else if($desc.value !== null && $desc.value !== undefined) {
|
|
55
|
+
props['aria-valuenow'] = String($desc.value);
|
|
56
|
+
}
|
|
57
|
+
|
|
49
58
|
const fillClass = ['progress-fill', 'is-' + variant];
|
|
50
59
|
|
|
51
60
|
if($desc.striped) {
|
|
@@ -26,6 +26,8 @@ export default function SkeletonRender($desc, instance) {
|
|
|
26
26
|
|
|
27
27
|
props.class.add('skeleton');
|
|
28
28
|
props.class.add('is-' + variant);
|
|
29
|
+
// [a11y] aria-label for screen readers
|
|
30
|
+
props['aria-label'] = $desc.label || 'Loading content';
|
|
29
31
|
props.class.add('is-' + ($desc.borderRadiusType || 'rounded'));
|
|
30
32
|
|
|
31
33
|
if(type === 'circle' || type === 'avatar') {
|
|
@@ -37,6 +37,9 @@ export default function SpinnerRender($desc, instance) {
|
|
|
37
37
|
|
|
38
38
|
const icon = Div({ class: 'spinner-icon', style: iconStyle });
|
|
39
39
|
|
|
40
|
+
// [a11y] aria-label: use label if provided, fallback to 'Loading'
|
|
41
|
+
props['aria-label'] = $desc.label || 'Loading';
|
|
42
|
+
|
|
40
43
|
const content = [icon];
|
|
41
44
|
|
|
42
45
|
if($desc.label) {
|
|
@@ -7,6 +7,10 @@ export default function SplitterGutterRender($desc, instance) {
|
|
|
7
7
|
props.class.add('is-' + ($desc.orientation || 'horizontal'));
|
|
8
8
|
props.class.add({'is-dragging': $desc.isDragging});
|
|
9
9
|
|
|
10
|
+
// [a11y] aria-orientation and aria-valuenow for resizable separator
|
|
11
|
+
props['aria-orientation'] = $desc.orientation === 'vertical' ? 'horizontal' : 'vertical';
|
|
12
|
+
props['tabindex'] = props['tabindex'] ?? '0';
|
|
13
|
+
|
|
10
14
|
// props.style.add({
|
|
11
15
|
// [($desc.orientation === 'vertical') ? 'height' : 'width']: ($desc.size || 8) + 'px',
|
|
12
16
|
// });
|
|
@@ -3,6 +3,10 @@ import {Div} from '../../../core/elements';
|
|
|
3
3
|
export default function SplitterPanelRender($desc, instance) {
|
|
4
4
|
const props = instance.getEditableProps();
|
|
5
5
|
props.class.add('splitter-panel');
|
|
6
|
+
// [a11y] aria-label if provided
|
|
7
|
+
if($desc.label) {
|
|
8
|
+
props['aria-label'] = $desc.label;
|
|
9
|
+
}
|
|
6
10
|
|
|
7
11
|
if($desc.collapsed) {
|
|
8
12
|
props.class.add('is-collapsed');
|
|
@@ -9,6 +9,8 @@ export default function SplitterRender($desc, instance) {
|
|
|
9
9
|
|
|
10
10
|
props.class.add('splitter');
|
|
11
11
|
props.class.add('is-' + ($desc.orientation || 'horizontal'));
|
|
12
|
+
// [a11y] aria-orientation
|
|
13
|
+
props['aria-orientation'] = $desc.orientation || 'horizontal';
|
|
12
14
|
|
|
13
15
|
const container = Div(instance.resolveProps());
|
|
14
16
|
|
|
@@ -24,7 +24,8 @@ export default function StepperRender($desc, instance) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const buildNav = ($desc, instance) => {
|
|
27
|
-
|
|
27
|
+
// [a11y] aria-label on stepper nav
|
|
28
|
+
return Div({ class: 'stepper-nav', 'aria-label': $desc.label || 'Steps' },
|
|
28
29
|
ForEachArray($desc.visibleSteps, (step, index) => {
|
|
29
30
|
step.$setStepper(instance);
|
|
30
31
|
const items = [];
|
|
@@ -48,7 +48,14 @@ export default function StepperStepRender($desc, instance) {
|
|
|
48
48
|
labelContent.push(Span({class: 'step-optional'}, 'Optional'));
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
// [a11y] aria-current, aria-disabled on step
|
|
52
|
+
const resolvedProps = instance.resolveProps();
|
|
53
|
+
resolvedProps['aria-current'] = isActive.transform((a) => a ? 'step' : undefined);
|
|
54
|
+
if($desc.disabled?.val()) {
|
|
55
|
+
resolvedProps['aria-disabled'] = 'true';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const stepEl = Div(resolvedProps, [
|
|
52
59
|
Div({class: indicatorClass}, [
|
|
53
60
|
indicatorContent,
|
|
54
61
|
Span({class: 'step-state-indicator'}, stateIndicator),
|
|
@@ -25,6 +25,9 @@ export default function SwitchRender($desc, instance) {
|
|
|
25
25
|
type: 'checkbox',
|
|
26
26
|
checked: $desc.value,
|
|
27
27
|
disabled: $desc.disabled,
|
|
28
|
+
// [a11y] aria-checked and aria-disabled
|
|
29
|
+
'aria-checked': $desc.value,
|
|
30
|
+
...($desc.disabled ? { 'aria-disabled': $desc.disabled } : {}),
|
|
28
31
|
});
|
|
29
32
|
|
|
30
33
|
$desc.value.subscribe(() => {
|
|
@@ -9,6 +9,10 @@ import {buildPageInfo, buildPageNav, buildPageSize, buildPagination} from './pag
|
|
|
9
9
|
export default function DataTableRender($desc, instance) {
|
|
10
10
|
const props = instance.getEditableProps();
|
|
11
11
|
props.class.add('data-table');
|
|
12
|
+
// [a11y] aria-label on data table
|
|
13
|
+
if($desc.label) {
|
|
14
|
+
props['aria-label'] = $desc.label;
|
|
15
|
+
}
|
|
12
16
|
|
|
13
17
|
const visibleColumns = $desc.columns.filter(col =>
|
|
14
18
|
col.$description.visible !== false,
|
|
@@ -46,6 +46,8 @@ const buildHeaderRow = ($desc, visibleColumns) => {
|
|
|
46
46
|
class: col.$description.align ? `is-${col.$description.align}` : null,
|
|
47
47
|
colspan: col.$description.colspan || null,
|
|
48
48
|
rowspan: col.$description.rowspan || null,
|
|
49
|
+
// [a11y] scope=col on header cells
|
|
50
|
+
scope: 'col',
|
|
49
51
|
...extraProps,
|
|
50
52
|
}, col.$description.header);
|
|
51
53
|
}),
|
|
@@ -26,6 +26,10 @@ export default function ToastRender($desc, instance) {
|
|
|
26
26
|
props.class.add('toast');
|
|
27
27
|
props.class.add('is-' + type);
|
|
28
28
|
|
|
29
|
+
// [a11y] aria-live: assertive for error, polite for others
|
|
30
|
+
props['aria-live'] = (type === 'error' || type === 'danger') ? 'assertive' : 'polite';
|
|
31
|
+
props['aria-atomic'] = 'true';
|
|
32
|
+
|
|
29
33
|
const content = [];
|
|
30
34
|
|
|
31
35
|
if($desc.showIcon && $desc.icon) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applies aria attributes on a trigger element for overlay components.
|
|
3
|
+
* Sets aria-haspopup and keeps aria-expanded in sync with the isOpen observable.
|
|
4
|
+
*
|
|
5
|
+
* @param {HTMLElement} triggerSource - The trigger element
|
|
6
|
+
* @param {ObservableItem<boolean>} isOpen - Observable controlling the open state
|
|
7
|
+
* @param {'menu'|'listbox'|'dialog'|'tree'|'grid'} [type='menu'] - aria-haspopup value
|
|
8
|
+
*/
|
|
9
|
+
export const ariaTrigger = (triggerSource, isOpen, type = 'menu') => {
|
|
10
|
+
const triggerEl = triggerSource?.toNdElement?.() ?? triggerSource;
|
|
11
|
+
if(!triggerEl) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
triggerEl.setAttribute('aria-haspopup', type);
|
|
15
|
+
triggerEl.setAttribute('aria-expanded', 'false');
|
|
16
|
+
isOpen.subscribe((value) => {
|
|
17
|
+
triggerEl.setAttribute('aria-expanded', String(value));
|
|
18
|
+
});
|
|
19
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { Observable } from '../../../src/core/data/Observable';
|
|
3
|
+
|
|
4
|
+
vi.mock('../../../src/core/utils/debug-manager', () => ({
|
|
5
|
+
default: { log: vi.fn(), warn: vi.fn(), error: vi.fn(), disable: vi.fn() },
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
import { Div, Input, Span } from '../../../elements';
|
|
9
|
+
|
|
10
|
+
describe('AttributesWrapper', () => {
|
|
11
|
+
|
|
12
|
+
// -- string attributes ----------------------------------------------------
|
|
13
|
+
|
|
14
|
+
describe('string attributes', () => {
|
|
15
|
+
it('sets a string attribute', () => {
|
|
16
|
+
const el = Div({ id: 'main', 'data-test': 'value' });
|
|
17
|
+
expect(el.getAttribute('id')).toBe('main');
|
|
18
|
+
expect(el.getAttribute('data-test')).toBe('value');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('sets a number attribute as string', () => {
|
|
22
|
+
const el = Div({ tabindex: 0 });
|
|
23
|
+
expect(el.getAttribute('tabindex')).toBe('0');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('ignores null values', () => {
|
|
27
|
+
const el = Div({ id: null });
|
|
28
|
+
expect(el.hasAttribute('id')).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('ignores undefined values', () => {
|
|
32
|
+
const el = Div({ id: undefined });
|
|
33
|
+
expect(el.hasAttribute('id')).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// -- class ----------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
describe('class', () => {
|
|
40
|
+
it('applies a class string', () => {
|
|
41
|
+
const el = Div({ class: 'foo bar' });
|
|
42
|
+
expect(el.className).toBe('foo bar');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('applies a class map object', () => {
|
|
46
|
+
const el = Div({ class: { active: true, disabled: false } });
|
|
47
|
+
expect(el.classList.contains('active')).toBe(true);
|
|
48
|
+
expect(el.classList.contains('disabled')).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('toggles class reactively with Observable', () => {
|
|
52
|
+
const isActive = Observable(false);
|
|
53
|
+
const el = Div({ class: { active: isActive } });
|
|
54
|
+
expect(el.classList.contains('active')).toBe(false);
|
|
55
|
+
isActive.set(true);
|
|
56
|
+
expect(el.classList.contains('active')).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('adds multiple reactive classes', () => {
|
|
60
|
+
const a = Observable(true);
|
|
61
|
+
const b = Observable(false);
|
|
62
|
+
const el = Div({ class: { foo: a, bar: b } });
|
|
63
|
+
expect(el.classList.contains('foo')).toBe(true);
|
|
64
|
+
expect(el.classList.contains('bar')).toBe(false);
|
|
65
|
+
b.set(true);
|
|
66
|
+
expect(el.classList.contains('bar')).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// -- style ----------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
describe('style', () => {
|
|
73
|
+
it('applies a static style map', () => {
|
|
74
|
+
const el = Div({ style: { color: 'red', fontSize: '16px' } });
|
|
75
|
+
expect(el.style.color).toBe('red');
|
|
76
|
+
expect(el.style.fontSize).toBe('16px');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('updates style reactively with Observable', () => {
|
|
80
|
+
const color = Observable('red');
|
|
81
|
+
const el = Div({ style: { color } });
|
|
82
|
+
expect(el.style.color).toBe('red');
|
|
83
|
+
color.set('blue');
|
|
84
|
+
expect(el.style.color).toBe('blue');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('sets CSS custom property via --var syntax', () => {
|
|
88
|
+
const el = Div({ style: { '--primary': '#fff' } });
|
|
89
|
+
expect(el.style.getPropertyValue('--primary')).toBe('#fff');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('updates CSS custom property reactively', () => {
|
|
93
|
+
const primary = Observable('#fff');
|
|
94
|
+
const el = Div({ style: { '--primary': primary } });
|
|
95
|
+
expect(el.style.getPropertyValue('--primary')).toBe('#fff');
|
|
96
|
+
primary.set('#000');
|
|
97
|
+
expect(el.style.getPropertyValue('--primary')).toBe('#000');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// -- Observable attribute -------------------------------------------------
|
|
102
|
+
|
|
103
|
+
describe('Observable attribute', () => {
|
|
104
|
+
it('sets attribute from Observable initial value', () => {
|
|
105
|
+
const label = Observable('hello');
|
|
106
|
+
const el = Div({ 'aria-label': label });
|
|
107
|
+
expect(el.getAttribute('aria-label')).toBe('hello');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('updates attribute when Observable changes', () => {
|
|
111
|
+
const label = Observable('hello');
|
|
112
|
+
const el = Div({ 'aria-label': label });
|
|
113
|
+
label.set('world');
|
|
114
|
+
expect(el.getAttribute('aria-label')).toBe('world');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('handles multiple Observable attributes', () => {
|
|
118
|
+
const a = Observable('val-a');
|
|
119
|
+
const b = Observable('val-b');
|
|
120
|
+
const el = Div({ 'data-a': a, 'data-b': b });
|
|
121
|
+
b.set('changed');
|
|
122
|
+
expect(el.getAttribute('data-a')).toBe('val-a');
|
|
123
|
+
expect(el.getAttribute('data-b')).toBe('changed');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// -- boolean attributes ---------------------------------------------------
|
|
128
|
+
|
|
129
|
+
describe('boolean attributes', () => {
|
|
130
|
+
it('sets disabled=true on input', () => {
|
|
131
|
+
const el = Input({ type: 'text', disabled: true });
|
|
132
|
+
expect(el.disabled).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('sets disabled=false on input', () => {
|
|
136
|
+
const el = Input({ type: 'text', disabled: false });
|
|
137
|
+
expect(el.disabled).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('toggles aria-disabled reactively via Observable', () => {
|
|
141
|
+
// Use a standard string attribute for reliable reactive testing
|
|
142
|
+
const isDisabled = Observable('false');
|
|
143
|
+
const el = Input({ type: 'text', 'aria-disabled': isDisabled });
|
|
144
|
+
expect(el.getAttribute('aria-disabled')).toBe('false');
|
|
145
|
+
isDisabled.set('true');
|
|
146
|
+
expect(el.getAttribute('aria-disabled')).toBe('true');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('sets checked on checkbox', () => {
|
|
150
|
+
const el = Input({ type: 'checkbox', checked: true });
|
|
151
|
+
expect(el.checked).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// -- aria attributes ------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
describe('aria attributes (via this.aria + resolveAriaKey)', () => {
|
|
158
|
+
it('sets aria-label directly', () => {
|
|
159
|
+
const el = Div({ 'aria-label': 'My label' });
|
|
160
|
+
expect(el.getAttribute('aria-label')).toBe('My label');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('sets role attribute', () => {
|
|
164
|
+
const el = Div({ role: 'dialog' });
|
|
165
|
+
expect(el.getAttribute('role')).toBe('dialog');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('sets aria-expanded reactively', () => {
|
|
169
|
+
const expanded = Observable('false');
|
|
170
|
+
const el = Div({ 'aria-expanded': expanded });
|
|
171
|
+
expect(el.getAttribute('aria-expanded')).toBe('false');
|
|
172
|
+
expanded.set('true');
|
|
173
|
+
expect(el.getAttribute('aria-expanded')).toBe('true');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
});
|