@nuvia-ui/components 4.0.1
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/package.json +27 -0
- package/src/ds-accordion/ds-accordion-item.js +288 -0
- package/src/ds-accordion/ds-accordion-item.stories.js +82 -0
- package/src/ds-accordion/ds-accordion.a11y.test.js +92 -0
- package/src/ds-accordion/ds-accordion.js +68 -0
- package/src/ds-accordion/ds-accordion.stories.js +118 -0
- package/src/ds-accordion/ds-accordion.test.js +146 -0
- package/src/ds-accordion/index.js +2 -0
- package/src/ds-action-bar/ds-action-bar.js +116 -0
- package/src/ds-action-bar/ds-action-bar.stories.js +86 -0
- package/src/ds-action-bar/ds-action-bar.test.js +64 -0
- package/src/ds-action-bar/index.js +1 -0
- package/src/ds-alert/ds-alert.a11y.test.js +151 -0
- package/src/ds-alert/ds-alert.js +223 -0
- package/src/ds-alert/ds-alert.mdx +142 -0
- package/src/ds-alert/ds-alert.stories.js +166 -0
- package/src/ds-alert/ds-alert.test.js +256 -0
- package/src/ds-alert/index.js +1 -0
- package/src/ds-avatar/ds-avatar.a11y.test.js +45 -0
- package/src/ds-avatar/ds-avatar.js +216 -0
- package/src/ds-avatar/ds-avatar.stories.js +120 -0
- package/src/ds-avatar/ds-avatar.test.js +83 -0
- package/src/ds-avatar/index.js +1 -0
- package/src/ds-avatar-extended/ds-avatar-extended.a11y.test.js +29 -0
- package/src/ds-avatar-extended/ds-avatar-extended.js +108 -0
- package/src/ds-avatar-extended/ds-avatar-extended.stories.js +93 -0
- package/src/ds-avatar-extended/ds-avatar-extended.test.js +66 -0
- package/src/ds-avatar-extended/index.js +1 -0
- package/src/ds-banner/ds-banner.a11y.test.js +51 -0
- package/src/ds-banner/ds-banner.js +233 -0
- package/src/ds-banner/ds-banner.stories.js +185 -0
- package/src/ds-banner/ds-banner.test.js +116 -0
- package/src/ds-banner/index.js +1 -0
- package/src/ds-breadcrumb-item/ds-breadcrumb-item.js +135 -0
- package/src/ds-breadcrumb-item/ds-breadcrumb-item.stories.js +49 -0
- package/src/ds-breadcrumb-item/ds-breadcrumb-item.test.js +55 -0
- package/src/ds-breadcrumbs/ds-breadcrumbs.js +194 -0
- package/src/ds-breadcrumbs/ds-breadcrumbs.stories.js +54 -0
- package/src/ds-breadcrumbs/ds-breadcrumbs.test.js +33 -0
- package/src/ds-button/ds-button.a11y.test.js +49 -0
- package/src/ds-button/ds-button.js +205 -0
- package/src/ds-button/ds-button.mdx +141 -0
- package/src/ds-button/ds-button.stories.js +152 -0
- package/src/ds-button/ds-button.test.js +62 -0
- package/src/ds-button/index.js +1 -0
- package/src/ds-button-group/ds-button-group.js +82 -0
- package/src/ds-button-group/ds-button-group.mdx +39 -0
- package/src/ds-button-group/ds-button-group.stories.js +47 -0
- package/src/ds-button-group/ds-button-group.test.js +47 -0
- package/src/ds-button-group/index.js +1 -0
- package/src/ds-checkbox/ds-checkbox.a11y.test.js +79 -0
- package/src/ds-checkbox/ds-checkbox.js +271 -0
- package/src/ds-checkbox/ds-checkbox.stories.js +77 -0
- package/src/ds-checkbox/ds-checkbox.test.js +191 -0
- package/src/ds-checkbox/index.js +1 -0
- package/src/ds-checkbox-group/ds-checkbox-group.a11y.test.js +146 -0
- package/src/ds-checkbox-group/ds-checkbox-group.js +235 -0
- package/src/ds-checkbox-group/ds-checkbox-group.stories.js +210 -0
- package/src/ds-checkbox-group/ds-checkbox-group.test.js +150 -0
- package/src/ds-checkbox-group/index.js +1 -0
- package/src/ds-dialog/ds-dialog.js +466 -0
- package/src/ds-dialog/ds-dialog.stories.js +274 -0
- package/src/ds-dialog/ds-dialog.test.js +441 -0
- package/src/ds-dialog/index.js +1 -0
- package/src/ds-dropdown/ds-dropdown.a11y.test.js +80 -0
- package/src/ds-dropdown/ds-dropdown.js +891 -0
- package/src/ds-dropdown/ds-dropdown.stories.js +259 -0
- package/src/ds-dropdown/ds-dropdown.test.js +268 -0
- package/src/ds-dropdown/index.js +1 -0
- package/src/ds-dropdown-group/ds-dropdown-group.js +55 -0
- package/src/ds-dropdown-panel/ds-dropdown-panel.js +34 -0
- package/src/ds-file-uploaded/ds-file-uploaded.a11y.test.js +40 -0
- package/src/ds-file-uploaded/ds-file-uploaded.js +135 -0
- package/src/ds-file-uploaded/ds-file-uploaded.mdx +33 -0
- package/src/ds-file-uploaded/ds-file-uploaded.stories.js +81 -0
- package/src/ds-file-uploaded/ds-file-uploaded.test.js +85 -0
- package/src/ds-file-uploader/ds-file-uploader.a11y.test.js +61 -0
- package/src/ds-file-uploader/ds-file-uploader.js +442 -0
- package/src/ds-file-uploader/ds-file-uploader.mdx +44 -0
- package/src/ds-file-uploader/ds-file-uploader.stories.js +76 -0
- package/src/ds-file-uploader/ds-file-uploader.test.js +142 -0
- package/src/ds-header/ds-header.a11y.test.js +38 -0
- package/src/ds-header/ds-header.js +149 -0
- package/src/ds-header/ds-header.stories.js +63 -0
- package/src/ds-header/ds-header.test.js +52 -0
- package/src/ds-header/index.js +1 -0
- package/src/ds-header-nav/ds-header-nav.a11y.test.js +69 -0
- package/src/ds-header-nav/ds-header-nav.js +114 -0
- package/src/ds-header-nav/ds-header-nav.stories.js +17 -0
- package/src/ds-header-nav/ds-header-nav.test.js +93 -0
- package/src/ds-header-nav-item/ds-header-nav-item.a11y.test.js +71 -0
- package/src/ds-header-nav-item/ds-header-nav-item.js +124 -0
- package/src/ds-header-nav-item/ds-header-nav-item.stories.js +43 -0
- package/src/ds-header-nav-item/ds-header-nav-item.test.js +61 -0
- package/src/ds-icon/ds-icon.a11y.test.js +49 -0
- package/src/ds-icon/ds-icon.js +75 -0
- package/src/ds-icon/ds-icon.mdx +36 -0
- package/src/ds-icon/ds-icon.stories.js +88 -0
- package/src/ds-icon/ds-icon.test.js +97 -0
- package/src/ds-icon/index.js +1 -0
- package/src/ds-icon-button/ds-icon-button.a11y.test.js +55 -0
- package/src/ds-icon-button/ds-icon-button.js +224 -0
- package/src/ds-icon-button/ds-icon-button.mdx +131 -0
- package/src/ds-icon-button/ds-icon-button.stories.js +128 -0
- package/src/ds-icon-button/ds-icon-button.test.js +90 -0
- package/src/ds-icon-button/index.js +1 -0
- package/src/ds-input/ds-input.a11y.test.js +145 -0
- package/src/ds-input/ds-input.js +645 -0
- package/src/ds-input/ds-input.mdx +251 -0
- package/src/ds-input/ds-input.stories.js +298 -0
- package/src/ds-input/ds-input.test.js +792 -0
- package/src/ds-input/index.js +1 -0
- package/src/ds-link/ds-link.js +111 -0
- package/src/ds-link/ds-link.stories.js +56 -0
- package/src/ds-link/ds-link.test.js +74 -0
- package/src/ds-list-item/ds-list-item.a11y.test.js +39 -0
- package/src/ds-list-item/ds-list-item.js +292 -0
- package/src/ds-list-item/ds-list-item.stories.js +101 -0
- package/src/ds-list-item/ds-list-item.test.js +63 -0
- package/src/ds-menu/ds-menu.js +30 -0
- package/src/ds-menu/ds-menu.stories.js +120 -0
- package/src/ds-menu/ds-menu.test.js +123 -0
- package/src/ds-menu-group/ds-menu-group.js +101 -0
- package/src/ds-menu-group/ds-menu-group.stories.js +99 -0
- package/src/ds-nav-item/ds-nav-item.a11y.test.js +91 -0
- package/src/ds-nav-item/ds-nav-item.js +307 -0
- package/src/ds-nav-item/ds-nav-item.stories.js +99 -0
- package/src/ds-nav-item/ds-nav-item.test.js +169 -0
- package/src/ds-nav-item/index.js +1 -0
- package/src/ds-nav-vertical/ds-nav-vertical.a11y.test.js +69 -0
- package/src/ds-nav-vertical/ds-nav-vertical.js +173 -0
- package/src/ds-nav-vertical/ds-nav-vertical.stories.js +124 -0
- package/src/ds-nav-vertical/ds-nav-vertical.test.js +176 -0
- package/src/ds-nav-vertical/index.js +1 -0
- package/src/ds-pagination/ds-pagination.a11y.test.js +50 -0
- package/src/ds-pagination/ds-pagination.js +232 -0
- package/src/ds-pagination/ds-pagination.stories.js +63 -0
- package/src/ds-pagination/ds-pagination.test.js +141 -0
- package/src/ds-pagination/index.js +1 -0
- package/src/ds-progress-bar/ds-progress-bar.a11y.test.js +25 -0
- package/src/ds-progress-bar/ds-progress-bar.js +81 -0
- package/src/ds-progress-bar/ds-progress-bar.stories.js +69 -0
- package/src/ds-progress-bar/ds-progress-bar.test.js +60 -0
- package/src/ds-radio/ds-radio.a11y.test.js +69 -0
- package/src/ds-radio/ds-radio.js +240 -0
- package/src/ds-radio/ds-radio.stories.js +102 -0
- package/src/ds-radio/ds-radio.test.js +114 -0
- package/src/ds-radio/index.js +1 -0
- package/src/ds-radio-group/ds-radio-group.a11y.test.js +164 -0
- package/src/ds-radio-group/ds-radio-group.js +257 -0
- package/src/ds-radio-group/ds-radio-group.stories.js +247 -0
- package/src/ds-radio-group/ds-radio-group.test.js +194 -0
- package/src/ds-radio-group/index.js +1 -0
- package/src/ds-rich-list/ds-rich-list.js +246 -0
- package/src/ds-rich-list/ds-rich-list.stories.js +368 -0
- package/src/ds-rich-list/ds-rich-list.test.js +293 -0
- package/src/ds-rich-list-item/ds-rich-list-item.js +579 -0
- package/src/ds-rich-list-item/ds-rich-list-item.stories.js +197 -0
- package/src/ds-rich-list-item/ds-rich-list-item.test.js +434 -0
- package/src/ds-slider/ds-slider.js +399 -0
- package/src/ds-slider/ds-slider.stories.js +107 -0
- package/src/ds-slider/ds-slider.test.js +308 -0
- package/src/ds-spinner/ds-spinner.js +173 -0
- package/src/ds-spinner/ds-spinner.stories.js +52 -0
- package/src/ds-spinner/ds-spinner.test.js +50 -0
- package/src/ds-status-border/ds-status-border.js +88 -0
- package/src/ds-status-border/ds-status-border.stories.js +242 -0
- package/src/ds-status-border/ds-status-border.test.js +168 -0
- package/src/ds-stepper/ds-stepper.a11y.test.js +198 -0
- package/src/ds-stepper/ds-stepper.js +207 -0
- package/src/ds-stepper/ds-stepper.stories.js +530 -0
- package/src/ds-stepper/ds-stepper.test.js +311 -0
- package/src/ds-stepper-item/ds-stepper-item.js +485 -0
- package/src/ds-stepper-item/ds-stepper-item.stories.js +288 -0
- package/src/ds-switch/ds-switch.js +348 -0
- package/src/ds-switch/ds-switch.stories.js +145 -0
- package/src/ds-switch/ds-switch.test.js +226 -0
- package/src/ds-switch/index.js +1 -0
- package/src/ds-tab-item/ds-tab-item.js +341 -0
- package/src/ds-tab-item/ds-tab-item.stories.js +69 -0
- package/src/ds-tabs/ds-tab-panel.js +48 -0
- package/src/ds-tabs/ds-tabs.a11y.test.js +56 -0
- package/src/ds-tabs/ds-tabs.js +180 -0
- package/src/ds-tabs/ds-tabs.stories.js +152 -0
- package/src/ds-tabs/ds-tabs.test.js +306 -0
- package/src/ds-tabs/index.js +3 -0
- package/src/ds-tag-action/ds-tag-action.a11y.test.js +32 -0
- package/src/ds-tag-action/ds-tag-action.js +185 -0
- package/src/ds-tag-action/ds-tag-action.stories.js +55 -0
- package/src/ds-tag-action/ds-tag-action.test.js +44 -0
- package/src/ds-tag-removable/ds-tag-removable.a11y.test.js +24 -0
- package/src/ds-tag-removable/ds-tag-removable.js +146 -0
- package/src/ds-tag-removable/ds-tag-removable.stories.js +52 -0
- package/src/ds-tag-removable/ds-tag-removable.test.js +46 -0
- package/src/ds-tag-status/ds-tag-status.a11y.test.js +93 -0
- package/src/ds-tag-status/ds-tag-status.js +164 -0
- package/src/ds-tag-status/ds-tag-status.stories.js +200 -0
- package/src/ds-tag-status/ds-tag-status.test.js +140 -0
- package/src/ds-tag-status/index.js +1 -0
- package/src/ds-textarea/ds-textarea-clearable.test.js +89 -0
- package/src/ds-textarea/ds-textarea.a11y.test.js +66 -0
- package/src/ds-textarea/ds-textarea.js +505 -0
- package/src/ds-textarea/ds-textarea.stories.js +335 -0
- package/src/ds-textarea/ds-textarea.test.js +218 -0
- package/src/ds-textarea/index.js +1 -0
- package/src/ds-thumbnail/ds-thumbnail.js +207 -0
- package/src/ds-thumbnail/ds-thumbnail.stories.js +217 -0
- package/src/ds-thumbnail/ds-thumbnail.test.js +220 -0
- package/src/ds-toast/ds-toast-provider.js +110 -0
- package/src/ds-toast/ds-toast.a11y.test.js +34 -0
- package/src/ds-toast/ds-toast.js +243 -0
- package/src/ds-toast/ds-toast.stories.js +143 -0
- package/src/ds-toast/ds-toast.test.js +93 -0
- package/src/ds-toast/index.js +2 -0
- package/src/ds-tooltip/ds-tooltip.a11y.test.js +110 -0
- package/src/ds-tooltip/ds-tooltip.js +217 -0
- package/src/ds-tooltip/ds-tooltip.mdx +75 -0
- package/src/ds-tooltip/ds-tooltip.stories.js +72 -0
- package/src/ds-tooltip/ds-tooltip.test.js +191 -0
- package/src/ds-tooltip/index.js +1 -0
- package/src/ds-tooltip/positioner.js +117 -0
- package/src/index.js +50 -0
- package/src/mixins/field-label.mixin.js +113 -0
- package/src/mixins/field-message.mixin.js +66 -0
- package/src/token-provider/index.js +1 -0
- package/src/token-provider/token-provider.a11y.test.js +44 -0
- package/src/token-provider/token-provider.js +85 -0
- package/src/token-provider/token-provider.stories.js +105 -0
- package/src/token-provider/token-provider.test.js +134 -0
- package/src/utils/number-input.utils.js +42 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import axe from 'axe-core';
|
|
3
|
+
import './ds-radio.js';
|
|
4
|
+
|
|
5
|
+
describe('ds-radio a11y', () => {
|
|
6
|
+
let container;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
container = document.createElement('div');
|
|
10
|
+
document.body.appendChild(container);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
container.remove();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should pass axe accessibility checks in default state', async () => {
|
|
18
|
+
container.innerHTML = '<ds-radio label="Option one"></ds-radio>';
|
|
19
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
20
|
+
|
|
21
|
+
const results = await axe.run(container, {
|
|
22
|
+
rules: { 'color-contrast': { enabled: false } }
|
|
23
|
+
});
|
|
24
|
+
if (results.violations.length > 0) {
|
|
25
|
+
console.log('Violations:', JSON.stringify(results.violations, null, 2));
|
|
26
|
+
}
|
|
27
|
+
expect(results.violations).toHaveLength(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should pass axe accessibility checks when checked', async () => {
|
|
31
|
+
container.innerHTML = '<ds-radio label="Selected option" checked></ds-radio>';
|
|
32
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
33
|
+
|
|
34
|
+
const results = await axe.run(container, {
|
|
35
|
+
rules: { 'color-contrast': { enabled: false } }
|
|
36
|
+
});
|
|
37
|
+
expect(results.violations).toHaveLength(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should pass axe accessibility checks when disabled', async () => {
|
|
41
|
+
container.innerHTML = '<ds-radio label="Disabled option" disabled></ds-radio>';
|
|
42
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
43
|
+
|
|
44
|
+
const results = await axe.run(container, {
|
|
45
|
+
rules: { 'color-contrast': { enabled: false } }
|
|
46
|
+
});
|
|
47
|
+
expect(results.violations).toHaveLength(0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should pass axe accessibility checks in error state', async () => {
|
|
51
|
+
container.innerHTML = '<ds-radio label="Error option" validation-status="error"></ds-radio>';
|
|
52
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
53
|
+
|
|
54
|
+
const results = await axe.run(container, {
|
|
55
|
+
rules: { 'color-contrast': { enabled: false } }
|
|
56
|
+
});
|
|
57
|
+
expect(results.violations).toHaveLength(0);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should pass axe accessibility checks when required', async () => {
|
|
61
|
+
container.innerHTML = '<ds-radio label="Required option" required></ds-radio>';
|
|
62
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
63
|
+
|
|
64
|
+
const results = await axe.run(container, {
|
|
65
|
+
rules: { 'color-contrast': { enabled: false } }
|
|
66
|
+
});
|
|
67
|
+
expect(results.violations).toHaveLength(0);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A radio button component for exclusive selection within a group.
|
|
5
|
+
*
|
|
6
|
+
* @element ds-radio
|
|
7
|
+
*
|
|
8
|
+
* @prop {Boolean} checked - Whether the radio is selected
|
|
9
|
+
* @prop {Boolean} disabled - Whether the radio is disabled
|
|
10
|
+
* @prop {Boolean} required - Whether the radio is required
|
|
11
|
+
* @prop {String} name - Name for form association (should match group)
|
|
12
|
+
* @prop {String} value - Value for form submission
|
|
13
|
+
* @prop {String} label - Label text displayed next to the radio
|
|
14
|
+
* @prop {String} validation-status - Validation state: 'error' or empty
|
|
15
|
+
*
|
|
16
|
+
* @fires change - Fired when the radio is selected
|
|
17
|
+
*/
|
|
18
|
+
export class DsRadio extends LitElement {
|
|
19
|
+
static properties = {
|
|
20
|
+
checked: { type: Boolean, reflect: true },
|
|
21
|
+
disabled: { type: Boolean, reflect: true },
|
|
22
|
+
required: { type: Boolean, reflect: true },
|
|
23
|
+
name: { type: String },
|
|
24
|
+
value: { type: String },
|
|
25
|
+
label: { type: String },
|
|
26
|
+
validationStatus: { type: String, attribute: 'validation-status', reflect: true }
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
static styles = css`
|
|
30
|
+
:host {
|
|
31
|
+
display: inline-flex;
|
|
32
|
+
align-items: flex-start;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Wrapper */
|
|
36
|
+
.radio-wrapper {
|
|
37
|
+
display: flex;
|
|
38
|
+
align-items: flex-start; /* Align to top to handle multiline labels if needed */
|
|
39
|
+
cursor: pointer;
|
|
40
|
+
padding: var(--ds-size-6) var(--ds-space-sm); /* 6px vertical (to get 32px total with 20px content), 8px horizontal */
|
|
41
|
+
min-height: var(--ds-size-32);
|
|
42
|
+
box-sizing: border-box;
|
|
43
|
+
border-radius: var(--ds-radius-container); /* 0px */
|
|
44
|
+
transition: background-color 0.2s;
|
|
45
|
+
position: relative;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
:host([disabled]) .radio-wrapper {
|
|
49
|
+
cursor: not-allowed;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* Hover state - Covers control and label */
|
|
53
|
+
:host(:not([disabled])) .radio-wrapper:hover {
|
|
54
|
+
background-color: var(--ds-color-bg-hover);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* Control Container - 20px height to match label line-height */
|
|
58
|
+
.control-container {
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
justify-content: center;
|
|
62
|
+
height: var(--ds-size-20);
|
|
63
|
+
flex-shrink: 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Control (The Circle) - 16px to match checkbox */
|
|
67
|
+
.control {
|
|
68
|
+
width: var(--ds-size-16);
|
|
69
|
+
height: var(--ds-size-16);
|
|
70
|
+
border: 2px solid var(--ds-color-border-strongest);
|
|
71
|
+
border-radius: var(--ds-size-999);
|
|
72
|
+
background-color: transparent;
|
|
73
|
+
display: flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
justify-content: center;
|
|
76
|
+
box-sizing: border-box;
|
|
77
|
+
transition: background-color 0.15s ease, border-color 0.15s ease;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* Inner Dot (shown when checked) - 8px for 16px control */
|
|
81
|
+
.dot {
|
|
82
|
+
width: 8px;
|
|
83
|
+
height: 8px;
|
|
84
|
+
border-radius: var(--ds-size-999);
|
|
85
|
+
background-color: var(--ds-color-bg-brand);
|
|
86
|
+
opacity: 0;
|
|
87
|
+
transform: scale(0);
|
|
88
|
+
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
:host([checked]) .dot {
|
|
92
|
+
opacity: 1;
|
|
93
|
+
transform: scale(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Checked State - keep border visible, dot handles the visual indicator */
|
|
97
|
+
:host([checked]) .control {
|
|
98
|
+
border-color: var(--ds-color-border-brand);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* Label Styles */
|
|
102
|
+
.label {
|
|
103
|
+
margin-inline-start: var(--ds-space-sm);
|
|
104
|
+
font: var(--ds-typo-content-body-regular);
|
|
105
|
+
color: var(--ds-color-text-default);
|
|
106
|
+
user-select: none;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Focus Visible */
|
|
110
|
+
.radio-wrapper:has(input:focus-visible) {
|
|
111
|
+
outline: 2px solid var(--ds-color-border-focus);
|
|
112
|
+
outline-offset: 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Native Input (Hidden but accessible) */
|
|
116
|
+
input {
|
|
117
|
+
position: absolute;
|
|
118
|
+
opacity: 0;
|
|
119
|
+
width: 100%;
|
|
120
|
+
height: 100%;
|
|
121
|
+
top: 0;
|
|
122
|
+
left: 0;
|
|
123
|
+
margin: 0;
|
|
124
|
+
cursor: inherit;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* Disabled State */
|
|
128
|
+
:host([disabled]) .radio-wrapper:hover {
|
|
129
|
+
background-color: transparent;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
:host([disabled]) .label {
|
|
133
|
+
color: var(--ds-color-text-disabled);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* Disabled State Control */
|
|
137
|
+
:host([disabled]) .control {
|
|
138
|
+
background-color: transparent;
|
|
139
|
+
border-color: var(--ds-color-icon-disabled);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
:host([disabled][checked]) .control {
|
|
143
|
+
background-color: var(--ds-color-bg-disabled);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* Disabled Dot Color */
|
|
147
|
+
:host([disabled]) .dot {
|
|
148
|
+
background-color: var(--ds-color-icon-disabled);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* Error State */
|
|
152
|
+
:host([validation-status="error"]) .control {
|
|
153
|
+
border-color: var(--ds-color-border-error);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
:host([validation-status="error"][checked]) .control {
|
|
157
|
+
border-color: var(--ds-color-border-error);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/* Error State Dot Color */
|
|
161
|
+
:host([validation-status="error"][checked]) .dot {
|
|
162
|
+
background-color: var(--ds-color-bg-error);
|
|
163
|
+
}
|
|
164
|
+
`;
|
|
165
|
+
|
|
166
|
+
constructor() {
|
|
167
|
+
super();
|
|
168
|
+
this.checked = false;
|
|
169
|
+
this.disabled = false;
|
|
170
|
+
this.required = false;
|
|
171
|
+
this.name = '';
|
|
172
|
+
this.value = '';
|
|
173
|
+
this.label = '';
|
|
174
|
+
this.validationStatus = '';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
_handleClick(e) {
|
|
178
|
+
if (this.disabled) return;
|
|
179
|
+
if (this.checked) return; // Radio buttons don't uncheck on click
|
|
180
|
+
|
|
181
|
+
e.preventDefault();
|
|
182
|
+
|
|
183
|
+
// Handle mutual exclusivity within the same root (Document or ShadowRoot)
|
|
184
|
+
const root = this.getRootNode();
|
|
185
|
+
const name = this.getAttribute('name');
|
|
186
|
+
|
|
187
|
+
if (name) {
|
|
188
|
+
const siblings = root.querySelectorAll(`ds-radio[name="${name}"]`);
|
|
189
|
+
siblings.forEach(sibling => {
|
|
190
|
+
if (sibling !== this) {
|
|
191
|
+
sibling.checked = false;
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
this.checked = true;
|
|
197
|
+
this._dispatchChange();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
_dispatchChange() {
|
|
201
|
+
this.dispatchEvent(new CustomEvent('change', {
|
|
202
|
+
detail: {
|
|
203
|
+
checked: this.checked,
|
|
204
|
+
value: this.value
|
|
205
|
+
},
|
|
206
|
+
bubbles: true,
|
|
207
|
+
composed: true
|
|
208
|
+
}));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
_getAriaLabel() {
|
|
212
|
+
return this.getAttribute('aria-label') || this.label || '';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
render() {
|
|
216
|
+
return html`
|
|
217
|
+
<div class="radio-wrapper" @click="${this._handleClick}">
|
|
218
|
+
<input
|
|
219
|
+
type="radio"
|
|
220
|
+
.checked="${this.checked}"
|
|
221
|
+
?disabled="${this.disabled}"
|
|
222
|
+
?required="${this.required}"
|
|
223
|
+
name="${this.name}"
|
|
224
|
+
value="${this.value}"
|
|
225
|
+
aria-label="${this._getAriaLabel()}"
|
|
226
|
+
>
|
|
227
|
+
|
|
228
|
+
<div class="control-container">
|
|
229
|
+
<div class="control">
|
|
230
|
+
<div class="dot"></div>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
${this.label ? html`<span class="label">${this.label}</span>` : ''}
|
|
235
|
+
</div>
|
|
236
|
+
`;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
customElements.define('ds-radio', DsRadio);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
import './ds-radio.js';
|
|
3
|
+
import '../token-provider/token-provider.js';
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
title: 'Components/Radio',
|
|
7
|
+
component: 'ds-radio',
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
label: { control: 'text' },
|
|
11
|
+
checked: { control: 'boolean' },
|
|
12
|
+
disabled: { control: 'boolean' },
|
|
13
|
+
required: { control: 'boolean' },
|
|
14
|
+
name: { control: 'text' },
|
|
15
|
+
value: { control: 'text' },
|
|
16
|
+
validationStatus: {
|
|
17
|
+
control: 'select',
|
|
18
|
+
options: ['', 'error']
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
args: {
|
|
22
|
+
label: 'Option label',
|
|
23
|
+
checked: false,
|
|
24
|
+
disabled: false,
|
|
25
|
+
required: false,
|
|
26
|
+
name: 'radio-group',
|
|
27
|
+
value: 'option-1',
|
|
28
|
+
validationStatus: ''
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const Default = {
|
|
33
|
+
args: {
|
|
34
|
+
label: 'Default option'
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const Checked = {
|
|
39
|
+
args: {
|
|
40
|
+
label: 'Selected option',
|
|
41
|
+
checked: true
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const Disabled = {
|
|
46
|
+
args: {
|
|
47
|
+
label: 'This option is disabled',
|
|
48
|
+
disabled: true
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const DisabledChecked = {
|
|
53
|
+
args: {
|
|
54
|
+
label: 'Disabled selected option',
|
|
55
|
+
checked: true,
|
|
56
|
+
disabled: true
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const WithError = {
|
|
61
|
+
args: {
|
|
62
|
+
label: 'Option with validation error',
|
|
63
|
+
validationStatus: 'error'
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const WithErrorChecked = {
|
|
68
|
+
args: {
|
|
69
|
+
label: 'Selected option with error',
|
|
70
|
+
checked: true,
|
|
71
|
+
validationStatus: 'error'
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const RadioGroup = {
|
|
76
|
+
render: () => html`
|
|
77
|
+
<div style="display: flex; flex-direction: column; gap: 8px;">
|
|
78
|
+
<ds-radio name="group-demo" value="option-1" label="First option" checked></ds-radio>
|
|
79
|
+
<ds-radio name="group-demo" value="option-2" label="Second option"></ds-radio>
|
|
80
|
+
<ds-radio name="group-demo" value="option-3" label="Third option"></ds-radio>
|
|
81
|
+
</div>
|
|
82
|
+
`
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const AllStates = {
|
|
86
|
+
render: () => html`
|
|
87
|
+
<div style="display: flex; flex-direction: column; gap: 16px;">
|
|
88
|
+
<div style="display: flex; gap: 24px; align-items: center;">
|
|
89
|
+
<ds-radio label="Default"></ds-radio>
|
|
90
|
+
<ds-radio label="Checked" checked></ds-radio>
|
|
91
|
+
</div>
|
|
92
|
+
<div style="display: flex; gap: 24px; align-items: center;">
|
|
93
|
+
<ds-radio label="Disabled" disabled></ds-radio>
|
|
94
|
+
<ds-radio label="Disabled checked" disabled checked></ds-radio>
|
|
95
|
+
</div>
|
|
96
|
+
<div style="display: flex; gap: 24px; align-items: center;">
|
|
97
|
+
<ds-radio label="Error" validation-status="error"></ds-radio>
|
|
98
|
+
<ds-radio label="Error checked" validation-status="error" checked></ds-radio>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
`
|
|
102
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import './ds-radio.js';
|
|
3
|
+
|
|
4
|
+
describe('ds-radio', () => {
|
|
5
|
+
let container;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
container = document.createElement('div');
|
|
9
|
+
document.body.appendChild(container);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
container.remove();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('renders with default values', async () => {
|
|
17
|
+
container.innerHTML = '<ds-radio></ds-radio>';
|
|
18
|
+
const element = container.querySelector('ds-radio');
|
|
19
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
20
|
+
|
|
21
|
+
expect(element.checked).toBe(false);
|
|
22
|
+
expect(element.disabled).toBe(false);
|
|
23
|
+
expect(element.shadowRoot.querySelector('.control')).toBeTruthy();
|
|
24
|
+
expect(element.shadowRoot.querySelector('.dot')).toBeTruthy();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('renders label when provided', async () => {
|
|
28
|
+
container.innerHTML = '<ds-radio label="Test Label"></ds-radio>';
|
|
29
|
+
const element = container.querySelector('ds-radio');
|
|
30
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
31
|
+
|
|
32
|
+
const label = element.shadowRoot.querySelector('.label');
|
|
33
|
+
expect(label).toBeTruthy();
|
|
34
|
+
expect(label.textContent).toBe('Test Label');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('reflects checked property to attribute', async () => {
|
|
38
|
+
container.innerHTML = '<ds-radio></ds-radio>';
|
|
39
|
+
const element = container.querySelector('ds-radio');
|
|
40
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
41
|
+
|
|
42
|
+
element.checked = true;
|
|
43
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
44
|
+
expect(element.hasAttribute('checked')).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('handles click events to select', async () => {
|
|
48
|
+
container.innerHTML = '<ds-radio></ds-radio>';
|
|
49
|
+
const element = container.querySelector('ds-radio');
|
|
50
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
51
|
+
|
|
52
|
+
const wrapper = element.shadowRoot.querySelector('.radio-wrapper');
|
|
53
|
+
|
|
54
|
+
// Click to check
|
|
55
|
+
wrapper.click();
|
|
56
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
57
|
+
expect(element.checked).toBe(true);
|
|
58
|
+
|
|
59
|
+
// Click again should NOT uncheck (radio behavior)
|
|
60
|
+
wrapper.click();
|
|
61
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
62
|
+
expect(element.checked).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('dispatches change event on interaction', async () => {
|
|
66
|
+
container.innerHTML = '<ds-radio value="test-value"></ds-radio>';
|
|
67
|
+
const element = container.querySelector('ds-radio');
|
|
68
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
69
|
+
|
|
70
|
+
let eventDetail = null;
|
|
71
|
+
element.addEventListener('change', (e) => {
|
|
72
|
+
eventDetail = e.detail;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const wrapper = element.shadowRoot.querySelector('.radio-wrapper');
|
|
76
|
+
wrapper.click();
|
|
77
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
78
|
+
|
|
79
|
+
expect(eventDetail).toBeTruthy();
|
|
80
|
+
expect(eventDetail.checked).toBe(true);
|
|
81
|
+
expect(eventDetail.value).toBe('test-value');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('does not toggle when disabled', async () => {
|
|
85
|
+
container.innerHTML = '<ds-radio disabled></ds-radio>';
|
|
86
|
+
const element = container.querySelector('ds-radio');
|
|
87
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
88
|
+
|
|
89
|
+
const wrapper = element.shadowRoot.querySelector('.radio-wrapper');
|
|
90
|
+
wrapper.click();
|
|
91
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
92
|
+
|
|
93
|
+
expect(element.checked).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('sets name and value on internal input', async () => {
|
|
97
|
+
container.innerHTML = '<ds-radio name="test-radio" value="accepted"></ds-radio>';
|
|
98
|
+
const element = container.querySelector('ds-radio');
|
|
99
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
100
|
+
|
|
101
|
+
const input = element.shadowRoot.querySelector('input');
|
|
102
|
+
expect(input.name).toBe('test-radio');
|
|
103
|
+
expect(input.value).toBe('accepted');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('uses aria-label from host if provided', async () => {
|
|
107
|
+
container.innerHTML = '<ds-radio aria-label="custom accessible name"></ds-radio>';
|
|
108
|
+
const element = container.querySelector('ds-radio');
|
|
109
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
110
|
+
|
|
111
|
+
const input = element.shadowRoot.querySelector('input');
|
|
112
|
+
expect(input.getAttribute('aria-label')).toBe('custom accessible name');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DsRadio } from './ds-radio.js';
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import axe from 'axe-core';
|
|
3
|
+
import './ds-radio-group.js';
|
|
4
|
+
import '../ds-radio/ds-radio.js';
|
|
5
|
+
|
|
6
|
+
describe('ds-radio-group a11y', () => {
|
|
7
|
+
let container;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
container = document.createElement('div');
|
|
11
|
+
document.body.appendChild(container);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
container.remove();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should pass axe accessibility checks with label and children', async () => {
|
|
19
|
+
container.innerHTML = `
|
|
20
|
+
<ds-radio-group label="Payment method" name="payment">
|
|
21
|
+
<ds-radio label="Credit card" value="credit"></ds-radio>
|
|
22
|
+
<ds-radio label="Debit card" value="debit"></ds-radio>
|
|
23
|
+
<ds-radio label="PayPal" value="paypal"></ds-radio>
|
|
24
|
+
</ds-radio-group>
|
|
25
|
+
`;
|
|
26
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
27
|
+
|
|
28
|
+
const results = await axe.run(container, {
|
|
29
|
+
rules: { 'color-contrast': { enabled: false } }
|
|
30
|
+
});
|
|
31
|
+
if (results.violations.length > 0) {
|
|
32
|
+
console.log('Violations:', JSON.stringify(results.violations, null, 2));
|
|
33
|
+
}
|
|
34
|
+
expect(results.violations).toHaveLength(0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should pass axe accessibility checks with helper text', async () => {
|
|
38
|
+
container.innerHTML = `
|
|
39
|
+
<ds-radio-group label="Shipping speed" name="shipping" helper="Select your preferred delivery option">
|
|
40
|
+
<ds-radio label="Standard (5-7 days)" value="standard"></ds-radio>
|
|
41
|
+
<ds-radio label="Express (2-3 days)" value="express"></ds-radio>
|
|
42
|
+
</ds-radio-group>
|
|
43
|
+
`;
|
|
44
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
45
|
+
|
|
46
|
+
const results = await axe.run(container, {
|
|
47
|
+
rules: { 'color-contrast': { enabled: false } }
|
|
48
|
+
});
|
|
49
|
+
expect(results.violations).toHaveLength(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should pass axe accessibility checks in error state', async () => {
|
|
53
|
+
container.innerHTML = `
|
|
54
|
+
<ds-radio-group
|
|
55
|
+
label="Required selection"
|
|
56
|
+
name="required"
|
|
57
|
+
validation-status="error"
|
|
58
|
+
validation-message="Please select an option"
|
|
59
|
+
>
|
|
60
|
+
<ds-radio label="Option A" value="a"></ds-radio>
|
|
61
|
+
<ds-radio label="Option B" value="b"></ds-radio>
|
|
62
|
+
</ds-radio-group>
|
|
63
|
+
`;
|
|
64
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
65
|
+
|
|
66
|
+
const results = await axe.run(container, {
|
|
67
|
+
rules: { 'color-contrast': { enabled: false } }
|
|
68
|
+
});
|
|
69
|
+
expect(results.violations).toHaveLength(0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should pass axe accessibility checks when disabled', async () => {
|
|
73
|
+
container.innerHTML = `
|
|
74
|
+
<ds-radio-group label="Disabled options" name="disabled" disabled>
|
|
75
|
+
<ds-radio label="Option A" value="a"></ds-radio>
|
|
76
|
+
<ds-radio label="Option B" value="b"></ds-radio>
|
|
77
|
+
</ds-radio-group>
|
|
78
|
+
`;
|
|
79
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
80
|
+
|
|
81
|
+
const results = await axe.run(container, {
|
|
82
|
+
rules: { 'color-contrast': { enabled: false } }
|
|
83
|
+
});
|
|
84
|
+
expect(results.violations).toHaveLength(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should pass axe accessibility checks with horizontal orientation', async () => {
|
|
88
|
+
container.innerHTML = `
|
|
89
|
+
<ds-radio-group label="Size" name="size" orientation="horizontal">
|
|
90
|
+
<ds-radio label="Small" value="s"></ds-radio>
|
|
91
|
+
<ds-radio label="Medium" value="m"></ds-radio>
|
|
92
|
+
<ds-radio label="Large" value="l"></ds-radio>
|
|
93
|
+
</ds-radio-group>
|
|
94
|
+
`;
|
|
95
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
96
|
+
|
|
97
|
+
const results = await axe.run(container, {
|
|
98
|
+
rules: { 'color-contrast': { enabled: false } }
|
|
99
|
+
});
|
|
100
|
+
expect(results.violations).toHaveLength(0);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should pass axe accessibility checks with pre-selected value', async () => {
|
|
104
|
+
container.innerHTML = `
|
|
105
|
+
<ds-radio-group label="Language" name="lang" value="en">
|
|
106
|
+
<ds-radio label="English" value="en"></ds-radio>
|
|
107
|
+
<ds-radio label="Portuguese" value="pt"></ds-radio>
|
|
108
|
+
<ds-radio label="Spanish" value="es"></ds-radio>
|
|
109
|
+
</ds-radio-group>
|
|
110
|
+
`;
|
|
111
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
112
|
+
|
|
113
|
+
const results = await axe.run(container, {
|
|
114
|
+
rules: { 'color-contrast': { enabled: false } }
|
|
115
|
+
});
|
|
116
|
+
expect(results.violations).toHaveLength(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should have proper radiogroup role attribute', async () => {
|
|
120
|
+
container.innerHTML = `
|
|
121
|
+
<ds-radio-group label="Test group" name="test">
|
|
122
|
+
<ds-radio label="Option A" value="a"></ds-radio>
|
|
123
|
+
</ds-radio-group>
|
|
124
|
+
`;
|
|
125
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
126
|
+
|
|
127
|
+
const group = container.querySelector('ds-radio-group');
|
|
128
|
+
const groupContent = group.shadowRoot.querySelector('.group-content');
|
|
129
|
+
|
|
130
|
+
expect(groupContent.getAttribute('role')).toBe('radiogroup');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should have aria-labelledby referencing the label', async () => {
|
|
134
|
+
container.innerHTML = `
|
|
135
|
+
<ds-radio-group label="Labeled group" name="labeled">
|
|
136
|
+
<ds-radio label="Option A" value="a"></ds-radio>
|
|
137
|
+
</ds-radio-group>
|
|
138
|
+
`;
|
|
139
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
140
|
+
|
|
141
|
+
const group = container.querySelector('ds-radio-group');
|
|
142
|
+
const groupContent = group.shadowRoot.querySelector('.group-content');
|
|
143
|
+
const labelId = groupContent.getAttribute('aria-labelledby');
|
|
144
|
+
|
|
145
|
+
expect(labelId).toBeTruthy();
|
|
146
|
+
expect(group.shadowRoot.getElementById(labelId)).toBeTruthy();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should propagate name to child radios for proper grouping', async () => {
|
|
150
|
+
container.innerHTML = `
|
|
151
|
+
<ds-radio-group label="Test group" name="test-group">
|
|
152
|
+
<ds-radio label="Option A" value="a"></ds-radio>
|
|
153
|
+
<ds-radio label="Option B" value="b"></ds-radio>
|
|
154
|
+
</ds-radio-group>
|
|
155
|
+
`;
|
|
156
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
157
|
+
|
|
158
|
+
const radios = container.querySelectorAll('ds-radio');
|
|
159
|
+
|
|
160
|
+
radios.forEach(radio => {
|
|
161
|
+
expect(radio.getAttribute('name')).toBe('test-group');
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|