@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 @@
|
|
|
1
|
+
export { DsInput } from './ds-input.js';
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { LitElement, html, css, nothing } from 'lit';
|
|
2
|
+
import '../ds-icon/ds-icon.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @element ds-link
|
|
6
|
+
* @summary A unified link component with intelligent defaults and consistent styling.
|
|
7
|
+
*
|
|
8
|
+
* @prop {string} href - Destination URL.
|
|
9
|
+
* @prop {string} target - Link target (e.g., '_blank').
|
|
10
|
+
* @prop {string} download - Download attribute.
|
|
11
|
+
* @prop {boolean} inline - Whether the link flows within text (default: true) or is standalone (false).
|
|
12
|
+
* @prop {string} icon-start - Icon to display before the text.
|
|
13
|
+
* @prop {string} icon-end - Icon to display after the text.
|
|
14
|
+
*
|
|
15
|
+
* @slot - Link text content.
|
|
16
|
+
*/
|
|
17
|
+
export class DsLink extends LitElement {
|
|
18
|
+
static properties = {
|
|
19
|
+
href: { type: String },
|
|
20
|
+
target: { type: String },
|
|
21
|
+
download: { type: String },
|
|
22
|
+
inline: { type: Boolean, reflect: true },
|
|
23
|
+
'icon-start': { type: String, attribute: 'icon-start' },
|
|
24
|
+
'icon-end': { type: String, attribute: 'icon-end' },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
static styles = css`
|
|
28
|
+
:host {
|
|
29
|
+
--link-decoration: underline;
|
|
30
|
+
display: inline; /* Default for inline flow */
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* Standalone Mode */
|
|
34
|
+
:host(:not([inline])) {
|
|
35
|
+
display: inline-flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
font: var(--ds-typo-content-body-regular); /* Fixed typography */
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
a {
|
|
41
|
+
color: var(--ds-color-text-link, #0052cc);
|
|
42
|
+
text-decoration: var(--link-decoration);
|
|
43
|
+
cursor: pointer;
|
|
44
|
+
outline: none;
|
|
45
|
+
|
|
46
|
+
/*
|
|
47
|
+
Trade-off:
|
|
48
|
+
- Inline links usually don't have icons.
|
|
49
|
+
- If they do, they behave like inline-blocks or inline-flex.
|
|
50
|
+
|
|
51
|
+
Let's stick to inline-flex for alignment, assuming short links or accepting block-like behavior for icon links.
|
|
52
|
+
*/
|
|
53
|
+
display: inline-flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
gap: var(--ds-space-xs, 4px);
|
|
56
|
+
vertical-align: baseline;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
a:hover {
|
|
60
|
+
color: var(--ds-color-text-link-hover, #003d99);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
a:focus-visible {
|
|
64
|
+
outline: 2px solid var(--ds-color-border-focus);
|
|
65
|
+
border-radius: var(--ds-radius-container, 0px);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
ds-icon {
|
|
69
|
+
color: inherit;
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
|
|
73
|
+
constructor() {
|
|
74
|
+
super();
|
|
75
|
+
this.inline = true;
|
|
76
|
+
this.href = '';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
render() {
|
|
80
|
+
const isExternal = this.target === '_blank';
|
|
81
|
+
|
|
82
|
+
// Security for external links
|
|
83
|
+
const rel = isExternal ? 'noreferrer noopener' : undefined;
|
|
84
|
+
|
|
85
|
+
// Logic for auto-icon:
|
|
86
|
+
// If target="_blank" AND no icon-end is provided, show 'open_in_new'.
|
|
87
|
+
// If icon-end IS provided, user override takes precedence.
|
|
88
|
+
let endIconName = this['icon-end'];
|
|
89
|
+
if (isExternal && !endIconName) {
|
|
90
|
+
endIconName = 'open-in-new';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return html`
|
|
94
|
+
<a
|
|
95
|
+
href=${this.href}
|
|
96
|
+
target=${this.target || nothing}
|
|
97
|
+
download=${this.download || nothing}
|
|
98
|
+
rel=${rel || nothing}
|
|
99
|
+
part="anchor"
|
|
100
|
+
>
|
|
101
|
+
${this['icon-start'] ? html`<ds-icon name=${this['icon-start']} size="sm"></ds-icon>` : nothing}
|
|
102
|
+
<slot></slot>
|
|
103
|
+
${endIconName ? html`<ds-icon name=${endIconName} size="sm"></ds-icon>` : nothing}
|
|
104
|
+
</a>
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!customElements.get('ds-link')) {
|
|
110
|
+
customElements.define('ds-link', DsLink);
|
|
111
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
import './ds-link.js';
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
title: 'Components/Link',
|
|
6
|
+
component: 'ds-link',
|
|
7
|
+
argTypes: {
|
|
8
|
+
href: { control: 'text' },
|
|
9
|
+
target: { control: 'text' },
|
|
10
|
+
inline: { control: 'boolean' },
|
|
11
|
+
'icon-start': { control: 'text' },
|
|
12
|
+
'icon-end': { control: 'text' },
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const Template = (args) => html`
|
|
17
|
+
<ds-link
|
|
18
|
+
.href=${args.href}
|
|
19
|
+
.target=${args.target}
|
|
20
|
+
.inline=${args.inline}
|
|
21
|
+
.icon-start=${args['icon-start']}
|
|
22
|
+
.icon-end=${args['icon-end']}
|
|
23
|
+
>
|
|
24
|
+
${args.label || 'Link Text'}
|
|
25
|
+
</ds-link>
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
export const DefaultInline = () => html`
|
|
29
|
+
<p style="font-family: sans-serif; line-height: 1.5;">
|
|
30
|
+
This is a paragraph with a
|
|
31
|
+
<ds-link href="#">default inline link</ds-link>
|
|
32
|
+
inside it. It inherits the text style.
|
|
33
|
+
</p>
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
export const Standalone = Template.bind({});
|
|
37
|
+
Standalone.args = {
|
|
38
|
+
inline: false,
|
|
39
|
+
href: '#',
|
|
40
|
+
label: 'Standalone Link',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const External = Template.bind({});
|
|
44
|
+
External.args = {
|
|
45
|
+
href: 'https://example.com',
|
|
46
|
+
target: '_blank',
|
|
47
|
+
label: 'External Link',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const WithIcons = Template.bind({});
|
|
51
|
+
WithIcons.args = {
|
|
52
|
+
href: '#',
|
|
53
|
+
label: 'Go Back',
|
|
54
|
+
'icon-start': 'arrow-back',
|
|
55
|
+
inline: false,
|
|
56
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import './ds-link.js';
|
|
3
|
+
|
|
4
|
+
describe('ds-link', () => {
|
|
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 attributes', async () => {
|
|
17
|
+
container.innerHTML = '<ds-link href="#">Click me</ds-link>';
|
|
18
|
+
const el = container.querySelector('ds-link');
|
|
19
|
+
await new Promise(r => setTimeout(r, 50));
|
|
20
|
+
|
|
21
|
+
const anchor = el.shadowRoot.querySelector('a');
|
|
22
|
+
expect(anchor).toBeTruthy();
|
|
23
|
+
expect(anchor.getAttribute('href')).toBe('#');
|
|
24
|
+
expect(el.inline).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('renders external link with security attributes and icon', async () => {
|
|
28
|
+
container.innerHTML = '<ds-link href="https://example.com" target="_blank">External</ds-link>';
|
|
29
|
+
const el = container.querySelector('ds-link');
|
|
30
|
+
await new Promise(r => setTimeout(r, 50));
|
|
31
|
+
|
|
32
|
+
const anchor = el.shadowRoot.querySelector('a');
|
|
33
|
+
expect(anchor.getAttribute('target')).toBe('_blank');
|
|
34
|
+
expect(anchor.getAttribute('rel')).toContain('noreferrer');
|
|
35
|
+
expect(anchor.getAttribute('rel')).toContain('noopener');
|
|
36
|
+
|
|
37
|
+
// Should render external icon automatically
|
|
38
|
+
const icons = el.shadowRoot.querySelectorAll('ds-icon');
|
|
39
|
+
expect(icons.length).toBe(1);
|
|
40
|
+
expect(icons[0].getAttribute('name')).toBe('open-in-new');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('allows overriding auto-icon with icon-end', async () => {
|
|
44
|
+
container.innerHTML = '<ds-link href="#" target="_blank" icon-end="star">Override</ds-link>';
|
|
45
|
+
const el = container.querySelector('ds-link');
|
|
46
|
+
await new Promise(r => setTimeout(r, 50));
|
|
47
|
+
|
|
48
|
+
const icons = el.shadowRoot.querySelectorAll('ds-icon');
|
|
49
|
+
expect(icons.length).toBe(1);
|
|
50
|
+
expect(icons[0].getAttribute('name')).toBe('star');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('applies standalone styling when inline is false', async () => {
|
|
54
|
+
// Lit prop setting via DOM property
|
|
55
|
+
const el = document.createElement('ds-link');
|
|
56
|
+
el.href = '#';
|
|
57
|
+
el.textContent = 'Standalone';
|
|
58
|
+
el.inline = false;
|
|
59
|
+
container.appendChild(el);
|
|
60
|
+
await new Promise(r => setTimeout(r, 50));
|
|
61
|
+
|
|
62
|
+
expect(el.hasAttribute('inline')).toBe(false);
|
|
63
|
+
expect(el.inline).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('renders start icon', async () => {
|
|
67
|
+
container.innerHTML = '<ds-link href="#" icon-start="home">Home</ds-link>';
|
|
68
|
+
const el = container.querySelector('ds-link');
|
|
69
|
+
await new Promise(r => setTimeout(r, 50));
|
|
70
|
+
|
|
71
|
+
const icon = el.shadowRoot.querySelector('ds-icon[name="home"]');
|
|
72
|
+
expect(icon).toBeTruthy();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import axe from 'axe-core';
|
|
3
|
+
import './ds-list-item.js';
|
|
4
|
+
|
|
5
|
+
describe('ds-list-item 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 be accessible as action', async () => {
|
|
18
|
+
container.innerHTML = `
|
|
19
|
+
<div role="list">
|
|
20
|
+
<ds-list-item label="Action Item"></ds-list-item>
|
|
21
|
+
</div>`;
|
|
22
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
23
|
+
const results = await axe.run(container);
|
|
24
|
+
expect(results.violations).toHaveLength(0);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should be accessible as option', async () => {
|
|
28
|
+
container.innerHTML = `
|
|
29
|
+
<div role="listbox" aria-label="Options Wrapper">
|
|
30
|
+
<ds-list-item variant="select-simple" label="Option 1"></ds-list-item>
|
|
31
|
+
</div>`;
|
|
32
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
33
|
+
const results = await axe.run(container);
|
|
34
|
+
if (results.violations.length > 0) {
|
|
35
|
+
console.log('Axe Violations:', JSON.stringify(results.violations, null, 2));
|
|
36
|
+
}
|
|
37
|
+
expect(results.violations).toHaveLength(0);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { LitElement, html, css, nothing } from 'lit';
|
|
2
|
+
import '../ds-icon/ds-icon.js';
|
|
3
|
+
import '../ds-checkbox/ds-checkbox.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Flexible List Item component for lists, menus, and dropdowns.
|
|
7
|
+
*
|
|
8
|
+
* @element ds-list-item
|
|
9
|
+
*
|
|
10
|
+
* @prop {string} label - Primary text
|
|
11
|
+
* @prop {string} additionalText - Secondary text
|
|
12
|
+
* @prop {string} variant - 'action' | 'select-checkbox' | 'select-simple' | 'section'
|
|
13
|
+
* @prop {boolean} selected - Selection state
|
|
14
|
+
* @prop {boolean} disabled - Disabled state
|
|
15
|
+
* @prop {boolean} divider - Show divider above (default for section)
|
|
16
|
+
*
|
|
17
|
+
* @slot start - Leading content (Icon, Avatar)
|
|
18
|
+
* @slot end - Trailing content (Icon, Badge)
|
|
19
|
+
* @slot - Default slot for label content (overrides label prop)
|
|
20
|
+
*
|
|
21
|
+
* @slot - Default slot for label content (overrides label prop)
|
|
22
|
+
*/
|
|
23
|
+
export class DsListItem extends LitElement {
|
|
24
|
+
static properties = {
|
|
25
|
+
label: { type: String },
|
|
26
|
+
additionalText: { type: String, attribute: 'additional-text' },
|
|
27
|
+
variant: { type: String, reflect: true },
|
|
28
|
+
selected: { type: Boolean, reflect: true },
|
|
29
|
+
indeterminate: { type: Boolean, reflect: true },
|
|
30
|
+
disabled: { type: Boolean, reflect: true },
|
|
31
|
+
divider: { type: Boolean, reflect: true },
|
|
32
|
+
hasStart: { type: Boolean, attribute: 'has-start', reflect: true },
|
|
33
|
+
hideLabel: { type: Boolean, attribute: 'hide-label', reflect: true },
|
|
34
|
+
hideLabel: { type: Boolean, attribute: 'hide-label', reflect: true },
|
|
35
|
+
hideDivider: { type: Boolean, attribute: 'hide-divider', reflect: true },
|
|
36
|
+
highlighted: { type: Boolean, reflect: true }
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
static styles = css`
|
|
40
|
+
:host {
|
|
41
|
+
display: block;
|
|
42
|
+
--list-item-min-height: 32px;
|
|
43
|
+
outline: none; /* Focus ring is handled on inner .item */
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Core Layout */
|
|
47
|
+
.item {
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: flex-start; /* Aligns content to top - better for multiline */
|
|
50
|
+
width: 100%;
|
|
51
|
+
min-height: var(--list-item-min-height);
|
|
52
|
+
height: auto;
|
|
53
|
+
box-sizing: border-box;
|
|
54
|
+
height: auto;
|
|
55
|
+
box-sizing: border-box;
|
|
56
|
+
padding: 6px 8px; /* Standard padding */
|
|
57
|
+
gap: 8px; /* Unified Spacing */
|
|
58
|
+
gap: 8px; /* Unified Spacing */
|
|
59
|
+
background: transparent;
|
|
60
|
+
color: var(--ds-color-text-default);
|
|
61
|
+
cursor: pointer;
|
|
62
|
+
text-decoration: none;
|
|
63
|
+
border: none;
|
|
64
|
+
outline: none;
|
|
65
|
+
position: relative;
|
|
66
|
+
transition: background-color 0.2s;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Hide Label Container */
|
|
70
|
+
:host([hide-label]) .item {
|
|
71
|
+
display: none;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Content Wrapper (Label + Additional) */
|
|
75
|
+
.content {
|
|
76
|
+
flex: 1;
|
|
77
|
+
display: flex;
|
|
78
|
+
flex-direction: column;
|
|
79
|
+
justify-content: center;
|
|
80
|
+
min-width: 0;
|
|
81
|
+
margin: 0; /* Align using gap */
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* Typography */
|
|
85
|
+
.label {
|
|
86
|
+
font: var(--ds-typo-content-body-regular);
|
|
87
|
+
color: var(--ds-color-text-default);
|
|
88
|
+
white-space: nowrap;
|
|
89
|
+
overflow: hidden;
|
|
90
|
+
text-overflow: ellipsis;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.additional-text {
|
|
94
|
+
font: var(--ds-typo-content-body-regular);
|
|
95
|
+
color: var(--ds-color-text-secondary);
|
|
96
|
+
white-space: nowrap;
|
|
97
|
+
overflow: hidden;
|
|
98
|
+
text-overflow: ellipsis;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* Variant: Section */
|
|
102
|
+
:host([variant="section"]) .item {
|
|
103
|
+
padding: 6px 8px; /* Same padding */
|
|
104
|
+
cursor: default;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
:host([variant="section"]) .label {
|
|
108
|
+
font: var(--ds-typo-content-body-bold);
|
|
109
|
+
color: var(--ds-color-text-secondary);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Divider */
|
|
113
|
+
:host([variant="section"]) {
|
|
114
|
+
display: block;
|
|
115
|
+
margin-top: 8px;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
:host([variant="section"]:not([hide-divider]))::before {
|
|
119
|
+
content: '';
|
|
120
|
+
display: block;
|
|
121
|
+
height: 1px;
|
|
122
|
+
background-color: var(--ds-color-border-default); /* Divider color */
|
|
123
|
+
margin-bottom: 8px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* Hide divider for named sections (Headers) per user request */
|
|
127
|
+
:host([variant="section"]:not([hide-label]))::before {
|
|
128
|
+
display: none;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* Selection Visuals */
|
|
132
|
+
.selection-visual {
|
|
133
|
+
display: flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
justify-content: center;
|
|
136
|
+
/* margin-right removed in favor of gap */
|
|
137
|
+
flex-shrink: 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* Slot Management */
|
|
141
|
+
:host(:not([has-start])) slot[name="start"] {
|
|
142
|
+
display: none;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* Checkbox Variant */
|
|
146
|
+
ds-checkbox {
|
|
147
|
+
pointer-events: none; /* Let the row handle click */
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* Simple Select Variant - Check Icon Container */
|
|
151
|
+
.check-icon-container {
|
|
152
|
+
width: 16px;
|
|
153
|
+
height: 20px;
|
|
154
|
+
display: flex;
|
|
155
|
+
align-items: center;
|
|
156
|
+
justify-content: center;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* States */
|
|
160
|
+
:host(:not([disabled]):not([variant="section"])) .item:hover {
|
|
161
|
+
background-color: var(--ds-color-bg-hover);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
:host(:not([disabled]):not([variant="section"])) .item:active {
|
|
165
|
+
background-color: var(--ds-color-bg-pressed);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* Highlighted state (Virtual Focus for aria-activedescendant) */
|
|
169
|
+
:host([highlighted]) .item {
|
|
170
|
+
background-color: var(--ds-color-bg-hover);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* Focus (Border Simulation via Outline on inner item when HOST is focused) */
|
|
174
|
+
:host(:focus-visible) .item {
|
|
175
|
+
outline: 2px solid var(--ds-color-border-focus);
|
|
176
|
+
outline-offset: -2px; /* Inner border effect */
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* Disabled */
|
|
180
|
+
:host([disabled]) .item {
|
|
181
|
+
background-color: var(--ds-color-bg-disabled);
|
|
182
|
+
color: var(--ds-color-text-disabled);
|
|
183
|
+
cursor: not-allowed;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
:host([disabled]) .label,
|
|
187
|
+
:host([disabled]) .additional-text {
|
|
188
|
+
color: var(--ds-color-text-disabled);
|
|
189
|
+
}
|
|
190
|
+
`;
|
|
191
|
+
|
|
192
|
+
constructor() {
|
|
193
|
+
super();
|
|
194
|
+
this.variant = 'action';
|
|
195
|
+
this.selected = false;
|
|
196
|
+
this.indeterminate = false;
|
|
197
|
+
this.disabled = false;
|
|
198
|
+
this.divider = false;
|
|
199
|
+
this.hasStart = false;
|
|
200
|
+
this.hideLabel = false;
|
|
201
|
+
this.hideDivider = false;
|
|
202
|
+
this.highlighted = false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Manage attributes on HOST to satisfy accessibility requirements
|
|
206
|
+
updated(changedProperties) {
|
|
207
|
+
if (changedProperties.has('variant') || changedProperties.has('selected') || changedProperties.has('disabled')) {
|
|
208
|
+
const role = this._getRole();
|
|
209
|
+
|
|
210
|
+
this.setAttribute('role', role);
|
|
211
|
+
|
|
212
|
+
if (role === 'option') {
|
|
213
|
+
this.setAttribute('aria-selected', this.selected ? 'true' : 'false');
|
|
214
|
+
} else {
|
|
215
|
+
this.removeAttribute('aria-selected');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (this.disabled || role === 'presentation') {
|
|
219
|
+
this.removeAttribute('tabindex');
|
|
220
|
+
} else {
|
|
221
|
+
if (!this.hasAttribute('tabindex')) {
|
|
222
|
+
this.setAttribute('tabindex', role === 'option' ? '-1' : '0');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
connectedCallback() {
|
|
229
|
+
super.connectedCallback();
|
|
230
|
+
this.requestUpdate();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
_getRole() {
|
|
234
|
+
if (this.variant === 'section') return 'presentation';
|
|
235
|
+
if (this.variant.startsWith('select')) return 'option';
|
|
236
|
+
return 'listitem';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
_handleSlotChange(e) {
|
|
240
|
+
const slot = e.target;
|
|
241
|
+
this.hasStart = slot.assignedNodes({ flatten: true }).length > 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
render() {
|
|
245
|
+
// Selection Element (Leftmost)
|
|
246
|
+
let selectionElement = nothing;
|
|
247
|
+
|
|
248
|
+
if (this.variant === 'select-checkbox') {
|
|
249
|
+
selectionElement = html`
|
|
250
|
+
<div class="selection-visual">
|
|
251
|
+
<ds-checkbox
|
|
252
|
+
standalone
|
|
253
|
+
?checked=${this.selected}
|
|
254
|
+
?indeterminate=${this.indeterminate}
|
|
255
|
+
?disabled=${this.disabled}
|
|
256
|
+
tabindex="-1"
|
|
257
|
+
></ds-checkbox>
|
|
258
|
+
</div>
|
|
259
|
+
`;
|
|
260
|
+
} else if (this.variant === 'select-simple' && this.selected) {
|
|
261
|
+
selectionElement = html`
|
|
262
|
+
<div class="selection-visual check-icon-container">
|
|
263
|
+
<ds-icon name="check" size="xs"></ds-icon>
|
|
264
|
+
</div>
|
|
265
|
+
`;
|
|
266
|
+
} else if (this.variant === 'select-simple' && !this.selected) {
|
|
267
|
+
selectionElement = html`
|
|
268
|
+
<div class="selection-visual check-icon-container"></div>
|
|
269
|
+
`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return html`
|
|
273
|
+
<div class="item">
|
|
274
|
+
${selectionElement}
|
|
275
|
+
|
|
276
|
+
<slot name="start" @slotchange="${this._handleSlotChange}"></slot>
|
|
277
|
+
|
|
278
|
+
<div class="content">
|
|
279
|
+
<span class="label">
|
|
280
|
+
${this.label || html`<slot></slot>`}
|
|
281
|
+
</span>
|
|
282
|
+
${this.additionalText ? html`<span class="additional-text">${this.additionalText}</span>` : nothing}
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
<slot name="end"></slot>
|
|
286
|
+
</div>
|
|
287
|
+
`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
customElements.define('ds-list-item', DsListItem);
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import './ds-list-item.js';
|
|
2
|
+
import '../ds-icon/ds-icon.js';
|
|
3
|
+
import '../ds-avatar/ds-avatar.js';
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
title: 'Components/List Item',
|
|
7
|
+
component: 'ds-list-item',
|
|
8
|
+
argTypes: {
|
|
9
|
+
label: { control: 'text' },
|
|
10
|
+
additionalText: { control: 'text' },
|
|
11
|
+
variant: {
|
|
12
|
+
control: 'select',
|
|
13
|
+
options: ['action', 'select-checkbox', 'select-simple', 'section']
|
|
14
|
+
},
|
|
15
|
+
selected: { control: 'boolean' },
|
|
16
|
+
disabled: { control: 'boolean' },
|
|
17
|
+
hideLabel: { control: 'boolean', attribute: 'hide-label' },
|
|
18
|
+
hideDivider: { control: 'boolean', attribute: 'hide-divider' }
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const Template = (args) => {
|
|
23
|
+
const el = document.createElement('ds-list-item');
|
|
24
|
+
Object.assign(el, args);
|
|
25
|
+
return el;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const Action = {
|
|
29
|
+
render: Template,
|
|
30
|
+
args: {
|
|
31
|
+
label: 'Profile Settings',
|
|
32
|
+
additionalText: 'Manage your account',
|
|
33
|
+
variant: 'action'
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const WithStartIcon = {
|
|
38
|
+
render: () => {
|
|
39
|
+
const el = document.createElement('ds-list-item');
|
|
40
|
+
el.label = 'Notifications';
|
|
41
|
+
|
|
42
|
+
const icon = document.createElement('ds-icon');
|
|
43
|
+
icon.setAttribute('slot', 'start');
|
|
44
|
+
icon.setAttribute('name', 'notifications');
|
|
45
|
+
icon.setAttribute('size', 'sm');
|
|
46
|
+
|
|
47
|
+
el.appendChild(icon);
|
|
48
|
+
return el;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const WithAvatarAndEndAction = {
|
|
53
|
+
render: () => {
|
|
54
|
+
const el = document.createElement('ds-list-item');
|
|
55
|
+
el.label = 'Miguel Silva';
|
|
56
|
+
el.additionalText = 'Online';
|
|
57
|
+
|
|
58
|
+
const avatar = document.createElement('ds-avatar');
|
|
59
|
+
avatar.setAttribute('slot', 'start');
|
|
60
|
+
avatar.setAttribute('initials', 'MS');
|
|
61
|
+
avatar.setAttribute('size', 'sm');
|
|
62
|
+
|
|
63
|
+
const icon = document.createElement('ds-icon');
|
|
64
|
+
icon.setAttribute('slot', 'end');
|
|
65
|
+
icon.setAttribute('name', 'chevron-right');
|
|
66
|
+
icon.setAttribute('size', 'sm');
|
|
67
|
+
|
|
68
|
+
el.appendChild(avatar);
|
|
69
|
+
el.appendChild(icon);
|
|
70
|
+
return el;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const SelectCheckbox = {
|
|
75
|
+
render: Template,
|
|
76
|
+
args: {
|
|
77
|
+
label: 'Enable Notifications',
|
|
78
|
+
variant: 'select-checkbox',
|
|
79
|
+
selected: true
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const SelectSimple = {
|
|
84
|
+
render: Template,
|
|
85
|
+
args: {
|
|
86
|
+
label: 'Selected Option',
|
|
87
|
+
variant: 'select-simple',
|
|
88
|
+
selected: true
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const Section = {
|
|
93
|
+
render: Template,
|
|
94
|
+
args: {
|
|
95
|
+
label: 'Section Header',
|
|
96
|
+
variant: 'section',
|
|
97
|
+
hideLabel: false,
|
|
98
|
+
hideDivider: false
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|