@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,207 @@
|
|
|
1
|
+
import { LitElement, html, css, nothing } from 'lit';
|
|
2
|
+
import '../ds-icon/ds-icon.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Thumbnail — a fluid image component with loading skeleton, fallback, and aspect ratio control.
|
|
6
|
+
*
|
|
7
|
+
* @element ds-thumbnail
|
|
8
|
+
* @summary Displays an image with skeleton loading state, error fallback, and aspect ratio support.
|
|
9
|
+
*
|
|
10
|
+
* @prop {string} src - Image source URL
|
|
11
|
+
* @prop {string} alt - Alt text for the image (required when src is set)
|
|
12
|
+
* @prop {string} aspect-ratio - Aspect ratio: '1:1' | '16:9' (default: '1:1')
|
|
13
|
+
* @prop {string} fit - Object-fit mode: 'cover' | 'contain' (default: 'cover')
|
|
14
|
+
* @prop {boolean} disabled - Whether the thumbnail is visually disabled
|
|
15
|
+
* @prop {string} icon - Fallback icon name (default: 'image')
|
|
16
|
+
*
|
|
17
|
+
* @fires ds-load - Dispatched when the image loads successfully
|
|
18
|
+
* @fires ds-error - Dispatched when the image fails to load
|
|
19
|
+
*
|
|
20
|
+
* @csspart container - The outer thumbnail container
|
|
21
|
+
* @csspart image - The img element
|
|
22
|
+
* @csspart fallback - The fallback icon container
|
|
23
|
+
*/
|
|
24
|
+
export class DsThumbnail extends LitElement {
|
|
25
|
+
static properties = {
|
|
26
|
+
src: { type: String },
|
|
27
|
+
alt: { type: String },
|
|
28
|
+
aspectRatio: { type: String, attribute: 'aspect-ratio', reflect: true },
|
|
29
|
+
fit: { type: String, reflect: true },
|
|
30
|
+
disabled: { type: Boolean, reflect: true },
|
|
31
|
+
icon: { type: String },
|
|
32
|
+
// Internal state
|
|
33
|
+
_state: { type: String, attribute: false } // 'empty' | 'loading' | 'loaded' | 'error'
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
static styles = css`
|
|
37
|
+
:host {
|
|
38
|
+
display: block;
|
|
39
|
+
width: 100%;
|
|
40
|
+
box-sizing: border-box;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* ─── Container ─── */
|
|
44
|
+
.thumbnail {
|
|
45
|
+
position: relative;
|
|
46
|
+
width: 100%;
|
|
47
|
+
overflow: hidden;
|
|
48
|
+
border-radius: var(--ds-radius-media, 4px);
|
|
49
|
+
background: var(--ds-color-bg-skeleton);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* ─── Aspect Ratios ─── */
|
|
53
|
+
:host([aspect-ratio="1:1"]) .thumbnail,
|
|
54
|
+
:host(:not([aspect-ratio])) .thumbnail {
|
|
55
|
+
aspect-ratio: 1 / 1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
:host([aspect-ratio="16:9"]) .thumbnail {
|
|
59
|
+
aspect-ratio: 16 / 9;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* ─── Image ─── */
|
|
63
|
+
.thumbnail__image {
|
|
64
|
+
position: absolute;
|
|
65
|
+
inset: 0;
|
|
66
|
+
width: 100%;
|
|
67
|
+
height: 100%;
|
|
68
|
+
display: block;
|
|
69
|
+
opacity: 0;
|
|
70
|
+
transition: opacity 0.25s ease;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
:host([fit="contain"]) .thumbnail__image {
|
|
74
|
+
object-fit: contain;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
:host(:not([fit="contain"])) .thumbnail__image {
|
|
78
|
+
object-fit: cover;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Image visible when loaded */
|
|
82
|
+
.thumbnail__image--loaded {
|
|
83
|
+
opacity: 1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* ─── Fallback ─── */
|
|
87
|
+
.thumbnail__fallback {
|
|
88
|
+
position: absolute;
|
|
89
|
+
inset: 0;
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
justify-content: center;
|
|
93
|
+
background: var(--ds-color-bg-secondary);
|
|
94
|
+
color: var(--ds-color-icon-secondary);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* ─── Skeleton Shimmer ─── */
|
|
98
|
+
.thumbnail__skeleton {
|
|
99
|
+
position: absolute;
|
|
100
|
+
inset: 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.thumbnail__skeleton::after {
|
|
104
|
+
content: '';
|
|
105
|
+
position: absolute;
|
|
106
|
+
inset: 0;
|
|
107
|
+
background: linear-gradient(
|
|
108
|
+
90deg,
|
|
109
|
+
transparent 25%,
|
|
110
|
+
rgba(255, 255, 255, 0.6) 50%,
|
|
111
|
+
transparent 75%
|
|
112
|
+
);
|
|
113
|
+
animation: shimmer 1.8s ease-in-out infinite;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@keyframes shimmer {
|
|
117
|
+
0% { transform: translateX(-150%); }
|
|
118
|
+
100% { transform: translateX(150%); }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* ─── Disabled ─── */
|
|
122
|
+
:host([disabled]) {
|
|
123
|
+
pointer-events: none;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
:host([disabled]) .thumbnail {
|
|
127
|
+
opacity: 0.5;
|
|
128
|
+
}
|
|
129
|
+
`;
|
|
130
|
+
|
|
131
|
+
constructor() {
|
|
132
|
+
super();
|
|
133
|
+
this.src = '';
|
|
134
|
+
this.alt = '';
|
|
135
|
+
this.aspectRatio = '1:1';
|
|
136
|
+
this.fit = 'cover';
|
|
137
|
+
this.disabled = false;
|
|
138
|
+
this.icon = 'image';
|
|
139
|
+
this._state = 'empty';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
willUpdate(changedProperties) {
|
|
143
|
+
if (changedProperties.has('src')) {
|
|
144
|
+
if (this.src) {
|
|
145
|
+
this._state = 'loading';
|
|
146
|
+
} else {
|
|
147
|
+
this._state = 'empty';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
_handleLoad() {
|
|
153
|
+
this._state = 'loaded';
|
|
154
|
+
this.dispatchEvent(new CustomEvent('ds-load', {
|
|
155
|
+
bubbles: true,
|
|
156
|
+
composed: true
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
_handleError() {
|
|
161
|
+
this._state = 'error';
|
|
162
|
+
this.dispatchEvent(new CustomEvent('ds-error', {
|
|
163
|
+
bubbles: true,
|
|
164
|
+
composed: true
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
_renderFallback() {
|
|
169
|
+
return html`
|
|
170
|
+
<div class="thumbnail__fallback" part="fallback">
|
|
171
|
+
<ds-icon name="${this.icon}" size="md"></ds-icon>
|
|
172
|
+
</div>
|
|
173
|
+
`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
_renderSkeleton() {
|
|
177
|
+
return html`<div class="thumbnail__skeleton"></div>`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
render() {
|
|
181
|
+
const showImage = this.src && (this._state === 'loading' || this._state === 'loaded');
|
|
182
|
+
const showFallback = this._state === 'empty' || this._state === 'error';
|
|
183
|
+
const showSkeleton = this._state === 'loading';
|
|
184
|
+
|
|
185
|
+
return html`
|
|
186
|
+
<div class="thumbnail" part="container">
|
|
187
|
+
${showFallback ? this._renderFallback() : nothing}
|
|
188
|
+
${showSkeleton ? this._renderSkeleton() : nothing}
|
|
189
|
+
${showImage ? html`
|
|
190
|
+
<img
|
|
191
|
+
class="thumbnail__image ${this._state === 'loaded' ? 'thumbnail__image--loaded' : ''}"
|
|
192
|
+
src="${this.src}"
|
|
193
|
+
alt="${this.alt}"
|
|
194
|
+
loading="lazy"
|
|
195
|
+
part="image"
|
|
196
|
+
@load=${this._handleLoad}
|
|
197
|
+
@error=${this._handleError}
|
|
198
|
+
/>
|
|
199
|
+
` : nothing}
|
|
200
|
+
</div>
|
|
201
|
+
`;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!customElements.get('ds-thumbnail')) {
|
|
206
|
+
customElements.define('ds-thumbnail', DsThumbnail);
|
|
207
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { html } from 'lit';
|
|
2
|
+
import './ds-thumbnail.js';
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
title: 'Components/Thumbnail',
|
|
6
|
+
component: 'ds-thumbnail',
|
|
7
|
+
tags: ['autodocs'],
|
|
8
|
+
argTypes: {
|
|
9
|
+
src: {
|
|
10
|
+
control: 'text',
|
|
11
|
+
description: 'Image URL'
|
|
12
|
+
},
|
|
13
|
+
alt: {
|
|
14
|
+
control: 'text',
|
|
15
|
+
description: 'Alt text for the image'
|
|
16
|
+
},
|
|
17
|
+
'aspect-ratio': {
|
|
18
|
+
control: { type: 'select' },
|
|
19
|
+
options: ['1:1', '16:9'],
|
|
20
|
+
description: 'Aspect ratio of the thumbnail'
|
|
21
|
+
},
|
|
22
|
+
fit: {
|
|
23
|
+
control: { type: 'select' },
|
|
24
|
+
options: ['cover', 'contain'],
|
|
25
|
+
description: 'How the image fits within the container'
|
|
26
|
+
},
|
|
27
|
+
disabled: {
|
|
28
|
+
control: 'boolean',
|
|
29
|
+
description: 'Whether the thumbnail is visually disabled'
|
|
30
|
+
},
|
|
31
|
+
icon: {
|
|
32
|
+
control: 'text',
|
|
33
|
+
description: 'Fallback icon name when no image is provided'
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// ─── Playground ───
|
|
39
|
+
export const Playground = {
|
|
40
|
+
args: {
|
|
41
|
+
src: 'https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=400&h=400&fit=crop',
|
|
42
|
+
alt: 'Beautiful landscape',
|
|
43
|
+
'aspect-ratio': '1:1',
|
|
44
|
+
fit: 'cover',
|
|
45
|
+
disabled: false,
|
|
46
|
+
icon: 'image'
|
|
47
|
+
},
|
|
48
|
+
render: (args) => html`
|
|
49
|
+
<div style="width: 200px;">
|
|
50
|
+
<ds-thumbnail
|
|
51
|
+
src="${args.src}"
|
|
52
|
+
alt="${args.alt}"
|
|
53
|
+
aspect-ratio="${args['aspect-ratio']}"
|
|
54
|
+
fit="${args.fit}"
|
|
55
|
+
?disabled="${args.disabled}"
|
|
56
|
+
icon="${args.icon}"
|
|
57
|
+
></ds-thumbnail>
|
|
58
|
+
</div>
|
|
59
|
+
`
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// ─── Loading (Skeleton) ───
|
|
63
|
+
export const Loading = {
|
|
64
|
+
render: () => html`
|
|
65
|
+
<div style="display: flex; gap: 24px; align-items: start;">
|
|
66
|
+
<div style="width: 120px;">
|
|
67
|
+
<p style="font: var(--ds-typo-content-caption-bold); color: var(--ds-color-text-secondary); margin: 0 0 8px;">1:1</p>
|
|
68
|
+
<ds-thumbnail id="skel-1" aspect-ratio="1:1"></ds-thumbnail>
|
|
69
|
+
</div>
|
|
70
|
+
<div style="width: 200px;">
|
|
71
|
+
<p style="font: var(--ds-typo-content-caption-bold); color: var(--ds-color-text-secondary); margin: 0 0 8px;">1:1 (larger)</p>
|
|
72
|
+
<ds-thumbnail id="skel-2" aspect-ratio="1:1"></ds-thumbnail>
|
|
73
|
+
</div>
|
|
74
|
+
<div style="width: 320px;">
|
|
75
|
+
<p style="font: var(--ds-typo-content-caption-bold); color: var(--ds-color-text-secondary); margin: 0 0 8px;">16:9</p>
|
|
76
|
+
<ds-thumbnail id="skel-3" aspect-ratio="16:9"></ds-thumbnail>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
`,
|
|
80
|
+
play: async ({ canvasElement }) => {
|
|
81
|
+
// Force all thumbnails into permanent loading state
|
|
82
|
+
// (no src set, so willUpdate won't override _state)
|
|
83
|
+
const thumbnails = canvasElement.querySelectorAll('ds-thumbnail');
|
|
84
|
+
for (const el of thumbnails) {
|
|
85
|
+
await el.updateComplete;
|
|
86
|
+
el._state = 'loading';
|
|
87
|
+
await el.updateComplete;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// ─── Aspect Ratios ───
|
|
93
|
+
export const AspectRatios = {
|
|
94
|
+
render: () => html`
|
|
95
|
+
<div style="display: flex; gap: 24px; align-items: start;">
|
|
96
|
+
<div style="width: 200px;">
|
|
97
|
+
<p style="font: var(--ds-typo-content-caption-bold); color: var(--ds-color-text-secondary); margin: 0 0 8px;">1:1 (Square)</p>
|
|
98
|
+
<ds-thumbnail
|
|
99
|
+
src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=400&h=400&fit=crop"
|
|
100
|
+
alt="Landscape 1:1"
|
|
101
|
+
aspect-ratio="1:1"
|
|
102
|
+
></ds-thumbnail>
|
|
103
|
+
</div>
|
|
104
|
+
<div style="width: 320px;">
|
|
105
|
+
<p style="font: var(--ds-typo-content-caption-bold); color: var(--ds-color-text-secondary); margin: 0 0 8px;">16:9 (Widescreen)</p>
|
|
106
|
+
<ds-thumbnail
|
|
107
|
+
src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=600&h=338&fit=crop"
|
|
108
|
+
alt="Landscape 16:9"
|
|
109
|
+
aspect-ratio="16:9"
|
|
110
|
+
></ds-thumbnail>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
`
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// ─── Object Fit ───
|
|
117
|
+
export const ObjectFit = {
|
|
118
|
+
render: () => html`
|
|
119
|
+
<div style="display: flex; gap: 24px; align-items: start;">
|
|
120
|
+
<div style="width: 200px;">
|
|
121
|
+
<p style="font: var(--ds-typo-content-caption-bold); color: var(--ds-color-text-secondary); margin: 0 0 8px;">Cover (default)</p>
|
|
122
|
+
<ds-thumbnail
|
|
123
|
+
src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=600&h=200&fit=crop"
|
|
124
|
+
alt="Cover example"
|
|
125
|
+
fit="cover"
|
|
126
|
+
></ds-thumbnail>
|
|
127
|
+
</div>
|
|
128
|
+
<div style="width: 200px;">
|
|
129
|
+
<p style="font: var(--ds-typo-content-caption-bold); color: var(--ds-color-text-secondary); margin: 0 0 8px;">Contain (logos)</p>
|
|
130
|
+
<ds-thumbnail
|
|
131
|
+
src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=600&h=200&fit=crop"
|
|
132
|
+
alt="Contain example"
|
|
133
|
+
fit="contain"
|
|
134
|
+
></ds-thumbnail>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
`
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// ─── Fallback (No Image) ───
|
|
141
|
+
export const Fallback = {
|
|
142
|
+
render: () => html`
|
|
143
|
+
<div style="display: flex; gap: 24px; align-items: start;">
|
|
144
|
+
<div style="width: 120px;">
|
|
145
|
+
<p style="font: var(--ds-typo-content-caption-bold); color: var(--ds-color-text-secondary); margin: 0 0 8px;">Default icon</p>
|
|
146
|
+
<ds-thumbnail></ds-thumbnail>
|
|
147
|
+
</div>
|
|
148
|
+
<div style="width: 120px;">
|
|
149
|
+
<p style="font: var(--ds-typo-content-caption-bold); color: var(--ds-color-text-secondary); margin: 0 0 8px;">Custom icon</p>
|
|
150
|
+
<ds-thumbnail icon="visibility"></ds-thumbnail>
|
|
151
|
+
</div>
|
|
152
|
+
<div style="width: 120px;">
|
|
153
|
+
<p style="font: var(--ds-typo-content-caption-bold); color: var(--ds-color-text-secondary); margin: 0 0 8px;">Broken src</p>
|
|
154
|
+
<ds-thumbnail src="https://broken-url.test/nope.jpg"></ds-thumbnail>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
`
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// ─── Sizes (Parent Controlled) ───
|
|
161
|
+
export const ParentControlledSizes = {
|
|
162
|
+
render: () => html`
|
|
163
|
+
<div style="display: flex; gap: 24px; align-items: start;">
|
|
164
|
+
<div style="width: 40px;">
|
|
165
|
+
<p style="font: var(--ds-typo-content-viz-regular); color: var(--ds-color-text-secondary); margin: 0 0 4px;">40px</p>
|
|
166
|
+
<ds-thumbnail
|
|
167
|
+
src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=100&h=100&fit=crop"
|
|
168
|
+
alt="Small"
|
|
169
|
+
></ds-thumbnail>
|
|
170
|
+
</div>
|
|
171
|
+
<div style="width: 80px;">
|
|
172
|
+
<p style="font: var(--ds-typo-content-viz-regular); color: var(--ds-color-text-secondary); margin: 0 0 4px;">80px</p>
|
|
173
|
+
<ds-thumbnail
|
|
174
|
+
src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=200&h=200&fit=crop"
|
|
175
|
+
alt="Medium"
|
|
176
|
+
></ds-thumbnail>
|
|
177
|
+
</div>
|
|
178
|
+
<div style="width: 160px;">
|
|
179
|
+
<p style="font: var(--ds-typo-content-viz-regular); color: var(--ds-color-text-secondary); margin: 0 0 4px;">160px</p>
|
|
180
|
+
<ds-thumbnail
|
|
181
|
+
src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=400&h=400&fit=crop"
|
|
182
|
+
alt="Large"
|
|
183
|
+
></ds-thumbnail>
|
|
184
|
+
</div>
|
|
185
|
+
<div style="width: 300px;">
|
|
186
|
+
<p style="font: var(--ds-typo-content-viz-regular); color: var(--ds-color-text-secondary); margin: 0 0 4px;">300px</p>
|
|
187
|
+
<ds-thumbnail
|
|
188
|
+
src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=600&h=600&fit=crop"
|
|
189
|
+
alt="XL"
|
|
190
|
+
></ds-thumbnail>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
`
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// ─── Disabled ───
|
|
197
|
+
export const Disabled = {
|
|
198
|
+
render: () => html`
|
|
199
|
+
<div style="display: flex; gap: 24px; align-items: start;">
|
|
200
|
+
<div style="width: 160px;">
|
|
201
|
+
<p style="font: var(--ds-typo-content-caption-bold); color: var(--ds-color-text-secondary); margin: 0 0 8px;">Enabled</p>
|
|
202
|
+
<ds-thumbnail
|
|
203
|
+
src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=400&h=400&fit=crop"
|
|
204
|
+
alt="Enabled"
|
|
205
|
+
></ds-thumbnail>
|
|
206
|
+
</div>
|
|
207
|
+
<div style="width: 160px;">
|
|
208
|
+
<p style="font: var(--ds-typo-content-caption-bold); color: var(--ds-color-text-secondary); margin: 0 0 8px;">Disabled</p>
|
|
209
|
+
<ds-thumbnail
|
|
210
|
+
src="https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=400&h=400&fit=crop"
|
|
211
|
+
alt="Disabled"
|
|
212
|
+
disabled
|
|
213
|
+
></ds-thumbnail>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
`
|
|
217
|
+
};
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import './ds-thumbnail.js';
|
|
3
|
+
|
|
4
|
+
describe('ds-thumbnail', () => {
|
|
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
|
+
async function createThumbnail(attrs = {}) {
|
|
17
|
+
const attrStr = Object.entries(attrs)
|
|
18
|
+
.map(([k, v]) => {
|
|
19
|
+
if (typeof v === 'boolean') return v ? k : '';
|
|
20
|
+
return `${k}="${v}"`;
|
|
21
|
+
})
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
.join(' ');
|
|
24
|
+
container.innerHTML = `<ds-thumbnail ${attrStr}></ds-thumbnail>`;
|
|
25
|
+
const el = container.querySelector('ds-thumbnail');
|
|
26
|
+
await el.updateComplete;
|
|
27
|
+
return el;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ─── Rendering ───
|
|
31
|
+
|
|
32
|
+
it('renders with default props', async () => {
|
|
33
|
+
const el = await createThumbnail();
|
|
34
|
+
expect(el).toBeTruthy();
|
|
35
|
+
expect(el.src).toBe('');
|
|
36
|
+
expect(el.alt).toBe('');
|
|
37
|
+
expect(el.aspectRatio).toBe('1:1');
|
|
38
|
+
expect(el.fit).toBe('cover');
|
|
39
|
+
expect(el.disabled).toBe(false);
|
|
40
|
+
expect(el.icon).toBe('image');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('renders fallback icon when no src is provided', async () => {
|
|
44
|
+
const el = await createThumbnail();
|
|
45
|
+
const fallback = el.shadowRoot.querySelector('.thumbnail__fallback');
|
|
46
|
+
expect(fallback).toBeTruthy();
|
|
47
|
+
const icon = fallback.querySelector('ds-icon');
|
|
48
|
+
expect(icon).toBeTruthy();
|
|
49
|
+
expect(icon.getAttribute('name')).toBe('image');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('renders custom fallback icon', async () => {
|
|
53
|
+
const el = await createThumbnail({ icon: 'photo_camera' });
|
|
54
|
+
const icon = el.shadowRoot.querySelector('.thumbnail__fallback ds-icon');
|
|
55
|
+
expect(icon.getAttribute('name')).toBe('photo_camera');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('does not render img element when no src', async () => {
|
|
59
|
+
const el = await createThumbnail();
|
|
60
|
+
const img = el.shadowRoot.querySelector('img');
|
|
61
|
+
expect(img).toBeFalsy();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ─── Image Loading ───
|
|
65
|
+
|
|
66
|
+
it('shows skeleton shimmer when src is set', async () => {
|
|
67
|
+
const el = await createThumbnail({ src: 'https://example.com/img.jpg' });
|
|
68
|
+
const skeleton = el.shadowRoot.querySelector('.thumbnail__skeleton');
|
|
69
|
+
expect(skeleton).toBeTruthy();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('renders img element when src is set', async () => {
|
|
73
|
+
const el = await createThumbnail({ src: 'https://example.com/img.jpg' });
|
|
74
|
+
const img = el.shadowRoot.querySelector('img');
|
|
75
|
+
expect(img).toBeTruthy();
|
|
76
|
+
expect(img.getAttribute('src')).toBe('https://example.com/img.jpg');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('sets loading="lazy" on img', async () => {
|
|
80
|
+
const el = await createThumbnail({ src: 'https://example.com/img.jpg' });
|
|
81
|
+
const img = el.shadowRoot.querySelector('img');
|
|
82
|
+
expect(img.getAttribute('loading')).toBe('lazy');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('sets alt attribute on img', async () => {
|
|
86
|
+
const el = await createThumbnail({ src: 'https://example.com/img.jpg', alt: 'Test alt' });
|
|
87
|
+
const img = el.shadowRoot.querySelector('img');
|
|
88
|
+
expect(img.getAttribute('alt')).toBe('Test alt');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('transitions to loaded state on image load', async () => {
|
|
92
|
+
const el = await createThumbnail({ src: 'https://example.com/img.jpg' });
|
|
93
|
+
const img = el.shadowRoot.querySelector('img');
|
|
94
|
+
img.dispatchEvent(new Event('load'));
|
|
95
|
+
await el.updateComplete;
|
|
96
|
+
|
|
97
|
+
expect(el._state).toBe('loaded');
|
|
98
|
+
expect(img.classList.contains('thumbnail__image--loaded')).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('hides skeleton after image loads', async () => {
|
|
102
|
+
const el = await createThumbnail({ src: 'https://example.com/img.jpg' });
|
|
103
|
+
const img = el.shadowRoot.querySelector('img');
|
|
104
|
+
img.dispatchEvent(new Event('load'));
|
|
105
|
+
await el.updateComplete;
|
|
106
|
+
|
|
107
|
+
const skeleton = el.shadowRoot.querySelector('.thumbnail__skeleton');
|
|
108
|
+
expect(skeleton).toBeFalsy();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('shows fallback on image error', async () => {
|
|
112
|
+
const el = await createThumbnail({ src: 'https://example.com/broken.jpg' });
|
|
113
|
+
const img = el.shadowRoot.querySelector('img');
|
|
114
|
+
img.dispatchEvent(new Event('error'));
|
|
115
|
+
await el.updateComplete;
|
|
116
|
+
|
|
117
|
+
expect(el._state).toBe('error');
|
|
118
|
+
const fallback = el.shadowRoot.querySelector('.thumbnail__fallback');
|
|
119
|
+
expect(fallback).toBeTruthy();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ─── Events ───
|
|
123
|
+
|
|
124
|
+
it('dispatches ds-load event on successful load', async () => {
|
|
125
|
+
const el = await createThumbnail({ src: 'https://example.com/img.jpg' });
|
|
126
|
+
let fired = false;
|
|
127
|
+
el.addEventListener('ds-load', () => { fired = true; });
|
|
128
|
+
|
|
129
|
+
const img = el.shadowRoot.querySelector('img');
|
|
130
|
+
img.dispatchEvent(new Event('load'));
|
|
131
|
+
|
|
132
|
+
expect(fired).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('dispatches ds-error event on image error', async () => {
|
|
136
|
+
const el = await createThumbnail({ src: 'https://example.com/broken.jpg' });
|
|
137
|
+
let fired = false;
|
|
138
|
+
el.addEventListener('ds-error', () => { fired = true; });
|
|
139
|
+
|
|
140
|
+
const img = el.shadowRoot.querySelector('img');
|
|
141
|
+
img.dispatchEvent(new Event('error'));
|
|
142
|
+
|
|
143
|
+
expect(fired).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// ─── Aspect Ratios ───
|
|
147
|
+
|
|
148
|
+
it('defaults to 1:1 aspect ratio', async () => {
|
|
149
|
+
const el = await createThumbnail();
|
|
150
|
+
expect(el.aspectRatio).toBe('1:1');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('supports 16:9 aspect ratio', async () => {
|
|
154
|
+
const el = await createThumbnail({ 'aspect-ratio': '16:9' });
|
|
155
|
+
expect(el.getAttribute('aspect-ratio')).toBe('16:9');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// ─── Object Fit ───
|
|
159
|
+
|
|
160
|
+
it('defaults to cover fit', async () => {
|
|
161
|
+
const el = await createThumbnail();
|
|
162
|
+
expect(el.fit).toBe('cover');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('supports contain fit', async () => {
|
|
166
|
+
const el = await createThumbnail({ fit: 'contain' });
|
|
167
|
+
expect(el.getAttribute('fit')).toBe('contain');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// ─── Disabled ───
|
|
171
|
+
|
|
172
|
+
it('applies disabled attribute', async () => {
|
|
173
|
+
const el = await createThumbnail({ disabled: true });
|
|
174
|
+
expect(el.hasAttribute('disabled')).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// ─── State Transitions ───
|
|
178
|
+
|
|
179
|
+
it('resets to loading when src changes', async () => {
|
|
180
|
+
const el = await createThumbnail({ src: 'https://example.com/1.jpg' });
|
|
181
|
+
|
|
182
|
+
// Simulate load
|
|
183
|
+
const img = el.shadowRoot.querySelector('img');
|
|
184
|
+
img.dispatchEvent(new Event('load'));
|
|
185
|
+
await el.updateComplete;
|
|
186
|
+
expect(el._state).toBe('loaded');
|
|
187
|
+
|
|
188
|
+
// Change src
|
|
189
|
+
el.src = 'https://example.com/2.jpg';
|
|
190
|
+
await el.updateComplete;
|
|
191
|
+
expect(el._state).toBe('loading');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('returns to empty when src is cleared', async () => {
|
|
195
|
+
const el = await createThumbnail({ src: 'https://example.com/1.jpg' });
|
|
196
|
+
el.src = '';
|
|
197
|
+
await el.updateComplete;
|
|
198
|
+
expect(el._state).toBe('empty');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// ─── CSS Parts ───
|
|
202
|
+
|
|
203
|
+
it('exposes container part', async () => {
|
|
204
|
+
const el = await createThumbnail();
|
|
205
|
+
const part = el.shadowRoot.querySelector('[part="container"]');
|
|
206
|
+
expect(part).toBeTruthy();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('exposes image part', async () => {
|
|
210
|
+
const el = await createThumbnail({ src: 'https://example.com/img.jpg' });
|
|
211
|
+
const part = el.shadowRoot.querySelector('[part="image"]');
|
|
212
|
+
expect(part).toBeTruthy();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('exposes fallback part', async () => {
|
|
216
|
+
const el = await createThumbnail();
|
|
217
|
+
const part = el.shadowRoot.querySelector('[part="fallback"]');
|
|
218
|
+
expect(part).toBeTruthy();
|
|
219
|
+
});
|
|
220
|
+
});
|