@kubex/zinc 1.1.98 → 1.1.99
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/AGENTS.md +5 -0
- package/CLAUDE.md +5 -0
- package/dist/custom-elements.json +2930 -2072
- package/dist/vscode.html-custom-data.json +198 -59
- package/dist/web-types.json +501 -207
- package/dist/zn.d.ts +203 -9
- package/dist/zn.min.js +343 -297
- package/docs/pages/components/background.md +143 -0
- package/docs/pages/components/checkbox-group.md +13 -1
- package/docs/pages/components/checkbox.md +59 -1
- package/docs/pages/components/radio-group.md +12 -0
- package/docs/pages/components/radio.md +188 -0
- package/docs/pages/components/remarkd-editor.md +22 -0
- package/docs/pages/components/scroll-container.md +14 -15
- package/package.json +1 -1
- package/src/components/background/background.component.ts +111 -0
- package/src/components/background/background.scss +236 -0
- package/src/components/background/background.test.ts +120 -0
- package/src/components/background/index.ts +12 -0
- package/src/components/checkbox/checkbox.component.ts +62 -6
- package/src/components/checkbox/checkbox.scss +1 -0
- package/src/components/checkbox/checkbox.test.ts +56 -0
- package/src/components/checkbox-group/checkbox-group.component.ts +15 -0
- package/src/components/checkbox-group/checkbox-group.scss +23 -5
- package/src/components/checkbox-group/checkbox-group.test.ts +15 -0
- package/src/components/confirm/confirm.component.ts +3 -3
- package/src/components/confirm/confirm.test.ts +15 -0
- package/src/components/dialog/dialog.component.ts +3 -2
- package/src/components/editor/editor.component.ts +7 -6
- package/src/components/header/header.scss +1 -1
- package/src/components/navbar/navbar.scss +11 -0
- package/src/components/radio/radio.component.ts +63 -7
- package/src/components/radio/radio.scss +1 -0
- package/src/components/radio/radio.test.ts +56 -0
- package/src/components/radio-group/radio-group.component.ts +16 -3
- package/src/components/radio-group/radio-group.scss +23 -4
- package/src/components/radio-group/radio-group.test.ts +15 -0
- package/src/components/remarkd-editor/remarkd-editor.component.ts +118 -5
- package/src/components/remarkd-editor/remarkd-editor.scss +8 -1
- package/src/components/remarkd-editor/remarkd-editor.test.ts +96 -0
- package/src/components/scroll-container/scroll-container.component.ts +15 -1
- package/src/components/scroll-container/scroll-container.scss +4 -2
- package/src/components/scroll-container/scroll-container.test.ts +13 -0
- package/src/components/slideout/slideout.component.ts +3 -2
- package/src/components/thumbnail-group/thumbnail-group.scss +0 -1
- package/src/components/tile/tile.scss +4 -2
- package/src/components/translation-group/translation-group.test.ts +22 -0
- package/src/components/translations/translations.component.ts +18 -3
- package/src/form-control.scss +3 -1
- package/src/internal/conditional.ts +2 -2
- package/src/internal/form.ts +2 -1
- package/src/internal/selection-card.ts +19 -0
- package/src/selection-card.scss +187 -0
- package/src/utilities/query.test.ts +18 -1
- package/src/utilities/query.ts +8 -0
- package/src/zinc.ts +1 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import {type CSSResultGroup, html, unsafeCSS} from 'lit';
|
|
2
|
+
import {property} from 'lit/decorators.js';
|
|
3
|
+
import {ResizeController} from '@lit-labs/observers/resize-controller.js';
|
|
4
|
+
import {styleMap} from 'lit/directives/style-map.js';
|
|
5
|
+
import ZincElement from '../../internal/zinc-element';
|
|
6
|
+
import ZnIcon from '../icon';
|
|
7
|
+
|
|
8
|
+
import styles from './background.scss';
|
|
9
|
+
|
|
10
|
+
export type BackgroundImageStrength = 'soft' | 'medium' | 'full';
|
|
11
|
+
export type BackgroundMotion = 'none' | 'drift' | 'breathe';
|
|
12
|
+
export type BackgroundOverlay = 'none' | 'soft' | 'strong';
|
|
13
|
+
export type BackgroundOverlayTone = 'light' | 'dark';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @summary Composes a colour, decorative image, image strength, optional motion and contrast overlay behind slotted content.
|
|
17
|
+
*
|
|
18
|
+
* @documentation https://zinc.style/components/background
|
|
19
|
+
* @status experimental
|
|
20
|
+
* @since 1.0
|
|
21
|
+
*
|
|
22
|
+
* @dependency zn-icon
|
|
23
|
+
*
|
|
24
|
+
* @slot - Content displayed above the background layers.
|
|
25
|
+
*
|
|
26
|
+
* @csspart base - The component's full-size background canvas.
|
|
27
|
+
* @csspart image - The decorative background image.
|
|
28
|
+
* @csspart overlay - The contrast overlay between the image and content.
|
|
29
|
+
* @csspart floating-icons - The non-interactive layer containing the floating icons.
|
|
30
|
+
* @csspart floating-icon - Each floating `zn-icon`.
|
|
31
|
+
* @csspart content - The wrapper around the default slot.
|
|
32
|
+
*
|
|
33
|
+
* @cssproperty --zn-background-color - Fallback colour when the `color` attribute is not set.
|
|
34
|
+
* @cssproperty --zn-background-image-position - Position of the background image. Defaults to `center`.
|
|
35
|
+
* @cssproperty --zn-background-overlay-angle - Direction of the overlay gradient. Defaults to `110deg`.
|
|
36
|
+
* @cssproperty --zn-background-floating-icon-color - Colour of the floating icons. Defaults to `currentColor`.
|
|
37
|
+
*/
|
|
38
|
+
export default class ZnBackground extends ZincElement {
|
|
39
|
+
static styles: CSSResultGroup = unsafeCSS(styles);
|
|
40
|
+
static dependencies = {'zn-icon': ZnIcon};
|
|
41
|
+
|
|
42
|
+
private readonly resizeObserver = new ResizeController<number>(this, {
|
|
43
|
+
callback: entries => {
|
|
44
|
+
const width = entries[0]?.contentRect.width ?? this.offsetWidth;
|
|
45
|
+
return Math.max(1, Math.min(width * 0.05, 150));
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** URL of the decorative background image. */
|
|
50
|
+
@property() image = '';
|
|
51
|
+
|
|
52
|
+
/** CSS colour painted beneath the image. */
|
|
53
|
+
@property() color = '';
|
|
54
|
+
|
|
55
|
+
/** Visibility of the image: `soft` (30%), `medium` (62%) or `full` (100%). */
|
|
56
|
+
@property({attribute: 'image-strength', reflect: true}) imageStrength: BackgroundImageStrength = 'full';
|
|
57
|
+
|
|
58
|
+
/** Subtle animation applied to the image. Motion stops when reduced motion is requested. */
|
|
59
|
+
@property({reflect: true}) motion: BackgroundMotion = 'none';
|
|
60
|
+
|
|
61
|
+
/** Strength of the contrast gradient above the image. */
|
|
62
|
+
@property({reflect: true}) overlay: BackgroundOverlay = 'soft';
|
|
63
|
+
|
|
64
|
+
/** Whether the overlay uses a light or dark contrast treatment. */
|
|
65
|
+
@property({attribute: 'overlay-tone', reflect: true}) overlayTone: BackgroundOverlayTone = 'light';
|
|
66
|
+
|
|
67
|
+
/** Comma-separated Zinc icon names placed as ambient decoration. At most eight are rendered. */
|
|
68
|
+
@property({attribute: 'floating-icons'}) floatingIcons = '';
|
|
69
|
+
|
|
70
|
+
/** Pauses background motion without changing the selected motion treatment. */
|
|
71
|
+
@property({type: Boolean, reflect: true}) paused = false;
|
|
72
|
+
|
|
73
|
+
render() {
|
|
74
|
+
const floatingIcons = this.floatingIcons
|
|
75
|
+
.split(',')
|
|
76
|
+
.map(icon => icon.trim())
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
.slice(0, 8);
|
|
79
|
+
const floatingIconSize = this.resizeObserver.value ?? 1;
|
|
80
|
+
|
|
81
|
+
return html`
|
|
82
|
+
<div
|
|
83
|
+
part="base"
|
|
84
|
+
class="background"
|
|
85
|
+
style=${styleMap({'--_zn-background-color': this.color || undefined})}>
|
|
86
|
+
${this.image ? html`
|
|
87
|
+
<img
|
|
88
|
+
part="image"
|
|
89
|
+
class="background__image"
|
|
90
|
+
src=${this.image}
|
|
91
|
+
alt=""
|
|
92
|
+
decoding="async">
|
|
93
|
+
` : ''}
|
|
94
|
+
<div part="overlay" class="background__overlay" aria-hidden="true"></div>
|
|
95
|
+
${floatingIcons.length ? html`
|
|
96
|
+
<div part="floating-icons" class="background__floating-icons" aria-hidden="true">
|
|
97
|
+
${floatingIcons.map(icon => html`
|
|
98
|
+
<zn-icon
|
|
99
|
+
part="floating-icon"
|
|
100
|
+
class="background__floating-icon"
|
|
101
|
+
src=${icon}
|
|
102
|
+
size=${floatingIconSize}
|
|
103
|
+
aria-hidden="true"></zn-icon>
|
|
104
|
+
`)}
|
|
105
|
+
</div>
|
|
106
|
+
` : ''}
|
|
107
|
+
<div part="content" class="background__content"><slot></slot></div>
|
|
108
|
+
</div>
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
@use "../../wc";
|
|
2
|
+
|
|
3
|
+
:host {
|
|
4
|
+
display: block;
|
|
5
|
+
position: relative;
|
|
6
|
+
min-width: 0;
|
|
7
|
+
min-height: 0;
|
|
8
|
+
overflow: hidden;
|
|
9
|
+
isolation: isolate;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.background {
|
|
13
|
+
position: relative;
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
width: 100%;
|
|
16
|
+
height: 100%;
|
|
17
|
+
min-height: inherit;
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
background-color: var(--_zn-background-color, var(--zn-background-color, transparent));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.background__image,
|
|
23
|
+
.background__overlay,
|
|
24
|
+
.background__floating-icons {
|
|
25
|
+
position: absolute;
|
|
26
|
+
inset: 0;
|
|
27
|
+
width: 100%;
|
|
28
|
+
height: 100%;
|
|
29
|
+
pointer-events: none;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.background__image {
|
|
33
|
+
width: 108%;
|
|
34
|
+
height: 108%;
|
|
35
|
+
max-width: none;
|
|
36
|
+
inset: -4%;
|
|
37
|
+
object-fit: cover;
|
|
38
|
+
object-position: var(--zn-background-image-position, center);
|
|
39
|
+
opacity: 1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
:host([image-strength="soft"]) .background__image {
|
|
43
|
+
opacity: 0.3;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
:host([image-strength="medium"]) .background__image {
|
|
47
|
+
opacity: 0.62;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.background__overlay {
|
|
51
|
+
background: linear-gradient(
|
|
52
|
+
var(--zn-background-overlay-angle, 110deg),
|
|
53
|
+
rgb(255 255 255 / 18%),
|
|
54
|
+
rgb(255 255 255 / 4%)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
:host([overlay="none"]) .background__overlay {
|
|
59
|
+
display: none;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
:host([overlay="strong"]) .background__overlay {
|
|
63
|
+
background: linear-gradient(
|
|
64
|
+
var(--zn-background-overlay-angle, 110deg),
|
|
65
|
+
rgb(255 255 255 / 48%),
|
|
66
|
+
rgb(255 255 255 / 18%)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
:host([overlay-tone="dark"]) .background__overlay {
|
|
71
|
+
background: linear-gradient(
|
|
72
|
+
var(--zn-background-overlay-angle, 110deg),
|
|
73
|
+
rgb(4 12 32 / 28%),
|
|
74
|
+
rgb(4 12 32 / 6%)
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
:host([overlay-tone="dark"][overlay="strong"]) .background__overlay {
|
|
79
|
+
background: linear-gradient(
|
|
80
|
+
var(--zn-background-overlay-angle, 110deg),
|
|
81
|
+
rgb(4 12 32 / 62%),
|
|
82
|
+
rgb(4 12 32 / 28%)
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
:host([motion="drift"]) .background__image {
|
|
87
|
+
animation: zn-background-drift 28s ease-in-out infinite alternate;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
:host([motion="breathe"]) .background__image {
|
|
91
|
+
animation: zn-background-breathe 18s ease-in-out infinite;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
:host([paused]) .background__image {
|
|
95
|
+
animation-play-state: paused;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.background__floating-icons {
|
|
99
|
+
overflow: hidden;
|
|
100
|
+
color: var(--zn-background-floating-icon-color, currentColor);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.background__floating-icon {
|
|
104
|
+
--_zn-background-icon-opacity-min: 0.01;
|
|
105
|
+
--_zn-background-icon-opacity-max: 0.18;
|
|
106
|
+
position: absolute;
|
|
107
|
+
left: 8%;
|
|
108
|
+
top: 13%;
|
|
109
|
+
opacity: var(--_zn-background-icon-opacity-min);
|
|
110
|
+
animation: zn-background-icon-float 11s ease-in-out infinite alternate both;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.background__floating-icon:nth-child(2) {
|
|
114
|
+
--_zn-background-icon-opacity-min: 0.08;
|
|
115
|
+
--_zn-background-icon-opacity-max: 0.34;
|
|
116
|
+
left: auto;
|
|
117
|
+
right: 9%;
|
|
118
|
+
top: 9%;
|
|
119
|
+
animation-delay: -4s;
|
|
120
|
+
animation-duration: 14s;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.background__floating-icon:nth-child(3) {
|
|
124
|
+
--_zn-background-icon-opacity-min: 0.03;
|
|
125
|
+
--_zn-background-icon-opacity-max: 0.5;
|
|
126
|
+
left: 2%;
|
|
127
|
+
top: 47%;
|
|
128
|
+
animation-delay: -7s;
|
|
129
|
+
animation-duration: 13s;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.background__floating-icon:nth-child(4) {
|
|
133
|
+
--_zn-background-icon-opacity-min: 0.12;
|
|
134
|
+
--_zn-background-icon-opacity-max: 0.38;
|
|
135
|
+
left: auto;
|
|
136
|
+
right: 12%;
|
|
137
|
+
top: 45%;
|
|
138
|
+
animation-delay: -2s;
|
|
139
|
+
animation-duration: 10s;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.background__floating-icon:nth-child(5) {
|
|
143
|
+
--_zn-background-icon-opacity-min: 0.05;
|
|
144
|
+
--_zn-background-icon-opacity-max: 0.29;
|
|
145
|
+
left: 21%;
|
|
146
|
+
top: auto;
|
|
147
|
+
bottom: 4%;
|
|
148
|
+
animation-delay: -5s;
|
|
149
|
+
animation-duration: 15s;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.background__floating-icon:nth-child(6) {
|
|
153
|
+
--_zn-background-icon-opacity-min: 0.01;
|
|
154
|
+
--_zn-background-icon-opacity-max: 0.42;
|
|
155
|
+
left: auto;
|
|
156
|
+
right: 4%;
|
|
157
|
+
top: auto;
|
|
158
|
+
bottom: 3%;
|
|
159
|
+
animation-delay: -9s;
|
|
160
|
+
animation-duration: 12s;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.background__floating-icon:nth-child(7) {
|
|
164
|
+
--_zn-background-icon-opacity-min: 0.09;
|
|
165
|
+
--_zn-background-icon-opacity-max: 0.31;
|
|
166
|
+
left: 42%;
|
|
167
|
+
top: 7%;
|
|
168
|
+
animation-delay: -6s;
|
|
169
|
+
animation-duration: 16s;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.background__floating-icon:nth-child(8) {
|
|
173
|
+
--_zn-background-icon-opacity-min: 0.04;
|
|
174
|
+
--_zn-background-icon-opacity-max: 0.46;
|
|
175
|
+
left: 54%;
|
|
176
|
+
top: auto;
|
|
177
|
+
bottom: 8%;
|
|
178
|
+
animation-delay: -11s;
|
|
179
|
+
animation-duration: 17s;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
:host([paused]) .background__floating-icon {
|
|
183
|
+
animation-play-state: paused;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.background__content {
|
|
187
|
+
position: relative;
|
|
188
|
+
z-index: 1;
|
|
189
|
+
box-sizing: border-box;
|
|
190
|
+
width: 100%;
|
|
191
|
+
height: 100%;
|
|
192
|
+
min-height: inherit;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@keyframes zn-background-drift {
|
|
196
|
+
from {
|
|
197
|
+
transform: scale(1.04) translate3d(-1.2%, -0.6%, 0);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
to {
|
|
201
|
+
transform: scale(1.04) translate3d(1.2%, 0.6%, 0);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@keyframes zn-background-breathe {
|
|
206
|
+
0%, 100% {
|
|
207
|
+
transform: scale(1.02);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
50% {
|
|
211
|
+
transform: scale(1.07);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@keyframes zn-background-icon-float {
|
|
216
|
+
from {
|
|
217
|
+
opacity: var(--_zn-background-icon-opacity-min);
|
|
218
|
+
transform: translate3d(-8%, -12%, 0) scale(0.88) rotate(-8deg);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
to {
|
|
222
|
+
opacity: var(--_zn-background-icon-opacity-max);
|
|
223
|
+
transform: translate3d(8%, 12%, 0) scale(1.06) rotate(8deg);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
@media (prefers-reduced-motion: reduce) {
|
|
228
|
+
.background__image,
|
|
229
|
+
.background__floating-icon {
|
|
230
|
+
animation: none;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.background__floating-icon {
|
|
234
|
+
opacity: var(--_zn-background-icon-opacity-max);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import '../../../dist/zn.min.js';
|
|
2
|
+
import {expect, fixture, html, waitUntil} from '@open-wc/testing';
|
|
3
|
+
import type ZnBackground from './background.component';
|
|
4
|
+
|
|
5
|
+
const transparentImage = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';
|
|
6
|
+
|
|
7
|
+
describe('<zn-background>', () => {
|
|
8
|
+
it('renders slotted content above the background layers', async () => {
|
|
9
|
+
const el = await fixture<ZnBackground>(html`
|
|
10
|
+
<zn-background><p>Lesson content</p></zn-background>`);
|
|
11
|
+
|
|
12
|
+
const content = el.shadowRoot!.querySelector<HTMLElement>('.background__content')!;
|
|
13
|
+
const slot = content.querySelector<HTMLSlotElement>('slot')!;
|
|
14
|
+
|
|
15
|
+
expect(slot.assignedElements()[0].textContent).to.equal('Lesson content');
|
|
16
|
+
expect(getComputedStyle(content).zIndex).to.equal('1');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('composes the configured colour and decorative image', async () => {
|
|
20
|
+
const el = await fixture<ZnBackground>(html`
|
|
21
|
+
<zn-background color="#123456" image=${transparentImage}></zn-background>`);
|
|
22
|
+
|
|
23
|
+
const base = el.shadowRoot!.querySelector<HTMLElement>('.background')!;
|
|
24
|
+
const image = el.shadowRoot!.querySelector<HTMLImageElement>('.background__image')!;
|
|
25
|
+
|
|
26
|
+
expect(getComputedStyle(base).backgroundColor).to.equal('rgb(18, 52, 86)');
|
|
27
|
+
expect(image.src).to.equal(transparentImage);
|
|
28
|
+
expect(image.alt).to.equal('');
|
|
29
|
+
expect(image.getAttribute('part')).to.equal('image');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('does not render an image element without an image URL', async () => {
|
|
33
|
+
const el = await fixture<ZnBackground>(html`<zn-background></zn-background>`);
|
|
34
|
+
|
|
35
|
+
expect(el.shadowRoot!.querySelector('.background__image')).to.not.exist;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('applies soft, medium and full image strengths', async () => {
|
|
39
|
+
const el = await fixture<ZnBackground>(html`
|
|
40
|
+
<zn-background image=${transparentImage} image-strength="soft"></zn-background>`);
|
|
41
|
+
const image = el.shadowRoot!.querySelector<HTMLImageElement>('.background__image')!;
|
|
42
|
+
|
|
43
|
+
expect(getComputedStyle(image).opacity).to.equal('0.3');
|
|
44
|
+
el.imageStrength = 'medium';
|
|
45
|
+
await el.updateComplete;
|
|
46
|
+
expect(getComputedStyle(image).opacity).to.equal('0.62');
|
|
47
|
+
el.imageStrength = 'full';
|
|
48
|
+
await el.updateComplete;
|
|
49
|
+
expect(getComputedStyle(image).opacity).to.equal('1');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('supports drift and breathe motion and can pause it', async () => {
|
|
53
|
+
const el = await fixture<ZnBackground>(html`
|
|
54
|
+
<zn-background image=${transparentImage} motion="drift"></zn-background>`);
|
|
55
|
+
const image = el.shadowRoot!.querySelector<HTMLImageElement>('.background__image')!;
|
|
56
|
+
|
|
57
|
+
expect(getComputedStyle(image).animationName).to.equal('zn-background-drift');
|
|
58
|
+
el.motion = 'breathe';
|
|
59
|
+
el.paused = true;
|
|
60
|
+
await el.updateComplete;
|
|
61
|
+
expect(getComputedStyle(image).animationName).to.equal('zn-background-breathe');
|
|
62
|
+
expect(getComputedStyle(image).animationPlayState).to.equal('paused');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('supports none, soft and strong light or dark overlays', async () => {
|
|
66
|
+
const el = await fixture<ZnBackground>(html`
|
|
67
|
+
<zn-background overlay="none"></zn-background>`);
|
|
68
|
+
const overlay = el.shadowRoot!.querySelector<HTMLElement>('.background__overlay')!;
|
|
69
|
+
|
|
70
|
+
expect(getComputedStyle(overlay).display).to.equal('none');
|
|
71
|
+
el.overlay = 'soft';
|
|
72
|
+
await el.updateComplete;
|
|
73
|
+
const lightGradient = getComputedStyle(overlay).backgroundImage;
|
|
74
|
+
expect(lightGradient).to.contain('255, 255, 255');
|
|
75
|
+
|
|
76
|
+
el.overlay = 'strong';
|
|
77
|
+
el.overlayTone = 'dark';
|
|
78
|
+
await el.updateComplete;
|
|
79
|
+
const darkGradient = getComputedStyle(overlay).backgroundImage;
|
|
80
|
+
expect(darkGradient).to.contain('4, 12, 32');
|
|
81
|
+
expect(darkGradient).to.not.equal(lightGradient);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('renders up to eight comma-separated floating Zinc icons', async () => {
|
|
85
|
+
const el = await fixture<ZnBackground>(html`
|
|
86
|
+
<zn-background
|
|
87
|
+
floating-icons="school, lightbulb, quiz, psychology, star, rocket, science, public, hidden">
|
|
88
|
+
</zn-background>`);
|
|
89
|
+
const icons = el.shadowRoot!.querySelectorAll<HTMLElement>('.background__floating-icon');
|
|
90
|
+
|
|
91
|
+
expect(icons).to.have.lengthOf(8);
|
|
92
|
+
expect(icons[0].getAttribute('src')).to.equal('school');
|
|
93
|
+
expect(icons[7].getAttribute('src')).to.equal('public');
|
|
94
|
+
expect(el.shadowRoot!.querySelector('.background__floating-icons')!.getAttribute('aria-hidden')).to.equal('true');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('sizes floating icons to five percent of the background width', async () => {
|
|
98
|
+
const el = await fixture<ZnBackground>(html`
|
|
99
|
+
<zn-background floating-icons="school" style="width: 1000px;"></zn-background>`);
|
|
100
|
+
const icon = el.shadowRoot!.querySelector<HTMLElement & {size: number}>('.background__floating-icon')!;
|
|
101
|
+
|
|
102
|
+
await waitUntil(() => icon.size === 50);
|
|
103
|
+
expect(icon.size).to.equal(50);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('caps floating icons at 150 pixels and keeps opacity within one to fifty percent', async () => {
|
|
107
|
+
const el = await fixture<ZnBackground>(html`
|
|
108
|
+
<zn-background floating-icons="school,lightbulb" style="width: 4000px;"></zn-background>`);
|
|
109
|
+
const icons = [...el.shadowRoot!.querySelectorAll<HTMLElement & {size: number}>('.background__floating-icon')];
|
|
110
|
+
|
|
111
|
+
await waitUntil(() => icons[0].size === 150);
|
|
112
|
+
expect(icons[0].size).to.equal(150);
|
|
113
|
+
icons.forEach(icon => {
|
|
114
|
+
const styles = getComputedStyle(icon);
|
|
115
|
+
expect(Number(styles.getPropertyValue('--_zn-background-icon-opacity-min'))).to.be.at.least(0.01);
|
|
116
|
+
expect(Number(styles.getPropertyValue('--_zn-background-icon-opacity-max'))).to.be.at.most(0.5);
|
|
117
|
+
expect(styles.animationName).to.equal('zn-background-icon-float');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import ZnBackground from './background.component';
|
|
2
|
+
|
|
3
|
+
export * from './background.component';
|
|
4
|
+
export default ZnBackground;
|
|
5
|
+
|
|
6
|
+
ZnBackground.define('zn-background');
|
|
7
|
+
|
|
8
|
+
declare global {
|
|
9
|
+
interface HTMLElementTagNameMap {
|
|
10
|
+
'zn-background': ZnBackground;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -9,6 +9,7 @@ import {property, query, state} from 'lit/decorators.js';
|
|
|
9
9
|
import {watch} from '../../internal/watch';
|
|
10
10
|
import ZincElement from '../../internal/zinc-element';
|
|
11
11
|
import ZnIcon from "../icon";
|
|
12
|
+
import type {SelectionCardControlPosition, SelectionCardImagePosition} from '../../internal/selection-card';
|
|
12
13
|
import type {ZincFormControl} from '../../internal/zinc-element';
|
|
13
14
|
|
|
14
15
|
import styles from './checkbox.scss';
|
|
@@ -24,6 +25,7 @@ type ColorOption = 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'suc
|
|
|
24
25
|
* @dependency zn-icon
|
|
25
26
|
*
|
|
26
27
|
* @slot - The checkbox's label.
|
|
28
|
+
* @slot image - Replaces the built-in image used by contained checkboxes.
|
|
27
29
|
* @slot description - A description of the checkbox's label. Serves as help text for a checkbox item. Alternatively, you can use the `description` attribute.
|
|
28
30
|
* @slot selected-content - Use to nest rich content (like an input) inside a selected checkbox item. Use only with the contained style.
|
|
29
31
|
*
|
|
@@ -40,9 +42,22 @@ type ColorOption = 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'suc
|
|
|
40
42
|
* @csspart checked-icon - The checked icon, an `<zn-icon>` element.
|
|
41
43
|
* @csspart unchecked-icon - The unchecked icon, an `<zn-icon>` element.
|
|
42
44
|
* @csspart indeterminate-icon - The indeterminate icon, an `<zn-icon>` element.
|
|
45
|
+
* @csspart image-container - The wrapper around the built-in image or image slot.
|
|
46
|
+
* @csspart image - The built-in image.
|
|
47
|
+
* @csspart card-title - The title inside a selection card.
|
|
43
48
|
* @csspart label - The container that wraps the checkbox's label.
|
|
44
49
|
* @csspart description - The container that wraps the checkbox's description.
|
|
45
50
|
* @csspart selected-content - The container that wraps optional content that appears when a checkbox is checked.
|
|
51
|
+
*
|
|
52
|
+
* @cssproperty --zn-selection-card-image-width - Width of the built-in card image.
|
|
53
|
+
* @cssproperty --zn-selection-card-image-height - Height of the built-in card image.
|
|
54
|
+
* @cssproperty --zn-selection-card-min-height - Minimum height of a contained checkbox with an image.
|
|
55
|
+
* @cssproperty --zn-selection-card-content-min-width - Width the card text keeps before it wraps below the image.
|
|
56
|
+
* @cssproperty --zn-selection-card-title-font-size - Font size of a selection card title.
|
|
57
|
+
* @cssproperty --zn-selection-card-padding - Equal inset around the contents of a selection card.
|
|
58
|
+
* @cssproperty --zn-selection-card-gap - Space between the image, title, and indicator gutter.
|
|
59
|
+
* @cssproperty --zn-selection-card-control-offset - Distance between a positioned control and the card edge.
|
|
60
|
+
* @cssproperty --zn-selection-card-border-radius - Corner radius of a contained container.
|
|
46
61
|
*/
|
|
47
62
|
export default class ZnCheckbox extends ZincElement implements ZincFormControl {
|
|
48
63
|
static styles: CSSResultGroup = unsafeCSS(styles);
|
|
@@ -53,7 +68,7 @@ export default class ZnCheckbox extends ZincElement implements ZincFormControl {
|
|
|
53
68
|
defaultValue: (control: ZnCheckbox) => control.defaultChecked,
|
|
54
69
|
setValue: (control: ZnCheckbox, checked: boolean) => (control.checked = checked)
|
|
55
70
|
});
|
|
56
|
-
private readonly hasSlotController = new HasSlotController(this, 'description');
|
|
71
|
+
private readonly hasSlotController = new HasSlotController(this, '[default]', 'description', 'image');
|
|
57
72
|
|
|
58
73
|
@query('input[type="checkbox"]') input: HTMLInputElement;
|
|
59
74
|
|
|
@@ -67,6 +82,9 @@ export default class ZnCheckbox extends ZincElement implements ZincFormControl {
|
|
|
67
82
|
/** The current value of the checkbox, submitted as a name/value pair with form data. */
|
|
68
83
|
@property() value: string;
|
|
69
84
|
|
|
85
|
+
/** Title rendered inside the card. The default slot takes precedence when provided. */
|
|
86
|
+
@property({attribute: 'card-title'}) cardTitle = '';
|
|
87
|
+
|
|
70
88
|
/** The unchecked value of the checkbox, submitted as a name/value pair with form data. */
|
|
71
89
|
@property({attribute: 'unchecked-value', reflect: true}) uncheckedValue: string;
|
|
72
90
|
|
|
@@ -88,6 +106,24 @@ export default class ZnCheckbox extends ZincElement implements ZincFormControl {
|
|
|
88
106
|
/** Draws a container around the checkbox. */
|
|
89
107
|
@property({type: Boolean, reflect: true}) contained = false;
|
|
90
108
|
|
|
109
|
+
/** Squares off the corners of the container drawn by `contained`, which is rounded by default. */
|
|
110
|
+
@property({type: Boolean, reflect: true}) square = false;
|
|
111
|
+
|
|
112
|
+
/** URL for the image shown in a contained checkbox. */
|
|
113
|
+
@property() src = '';
|
|
114
|
+
|
|
115
|
+
/** Accessible text for the built-in image. Leave empty when the image is decorative. */
|
|
116
|
+
@property({attribute: 'image-alt'}) imageAlt = '';
|
|
117
|
+
|
|
118
|
+
/** Places the image above, beside, or below the checkbox's text. Requires `contained`. */
|
|
119
|
+
@property({attribute: 'image-position', reflect: true}) imagePosition: SelectionCardImagePosition = 'left';
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Places the checkbox indicator within a contained card. Use `none` to hide the indicator so the card itself
|
|
123
|
+
* shows the selected state. Requires `contained`.
|
|
124
|
+
*/
|
|
125
|
+
@property({attribute: 'control-position', reflect: true}) controlPosition: SelectionCardControlPosition = 'start';
|
|
126
|
+
|
|
91
127
|
/** Removes a container around the checkbox. */
|
|
92
128
|
@property({type: Boolean, reflect: true}) borderless = false;
|
|
93
129
|
|
|
@@ -244,6 +280,9 @@ export default class ZnCheckbox extends ZincElement implements ZincFormControl {
|
|
|
244
280
|
render() {
|
|
245
281
|
const hasDescriptionSlot = this.hasSlotController.test('description');
|
|
246
282
|
const hasDescription = this.description ? true : hasDescriptionSlot;
|
|
283
|
+
const hasImage = this.contained && (Boolean(this.src) || this.hasSlotController.test('image'));
|
|
284
|
+
const hasDefaultSlot = this.hasSlotController.test('[default]');
|
|
285
|
+
const isCard = this.contained && (hasImage || Boolean(this.cardTitle) || this.controlPosition !== 'start');
|
|
247
286
|
const hasLabelSlot = this.hasSlotController.test('label');
|
|
248
287
|
const hasLabelTooltip = this.hasSlotController.test('label-tooltip');
|
|
249
288
|
const hasLabel = this.label || hasLabelSlot;
|
|
@@ -309,7 +348,11 @@ export default class ZnCheckbox extends ZincElement implements ZincFormControl {
|
|
|
309
348
|
'checkbox--warning': effectiveColor === 'warning',
|
|
310
349
|
'checkbox--transparent': effectiveColor === 'transparent',
|
|
311
350
|
'checkbox--has-description': hasDescription,
|
|
312
|
-
'checkbox--has-selected-content': this.hasSlotController.test('selected-content')
|
|
351
|
+
'checkbox--has-selected-content': this.hasSlotController.test('selected-content'),
|
|
352
|
+
'selection-card': isCard,
|
|
353
|
+
'selection-card--has-image': hasImage,
|
|
354
|
+
[`selection-card--image-${this.imagePosition}`]: hasImage,
|
|
355
|
+
[`selection-card--control-${this.controlPosition}`]: isCard && this.controlPosition !== 'start'
|
|
313
356
|
})}>
|
|
314
357
|
|
|
315
358
|
<input
|
|
@@ -323,7 +366,7 @@ export default class ZnCheckbox extends ZincElement implements ZincFormControl {
|
|
|
323
366
|
.disabled=${this.disabled}
|
|
324
367
|
.required=${this.required}
|
|
325
368
|
aria-checked=${this.checked ? 'true' : 'false'}
|
|
326
|
-
aria-describedby=${hasDescription ? '' : '
|
|
369
|
+
aria-describedby=${hasDescription ? 'description' : ''}
|
|
327
370
|
@click=${this.handleClick}
|
|
328
371
|
@input=${this.handleInput}
|
|
329
372
|
@invalid=${this.handleInvalid}
|
|
@@ -334,7 +377,7 @@ export default class ZnCheckbox extends ZincElement implements ZincFormControl {
|
|
|
334
377
|
part="control${this.checked ? ' control--checked' : ''}${this.indeterminate
|
|
335
378
|
? ' control--indeterminate'
|
|
336
379
|
: ''}"
|
|
337
|
-
class="checkbox__control">
|
|
380
|
+
class="checkbox__control selection-card__control">
|
|
338
381
|
|
|
339
382
|
${this.checked && !this.indeterminate
|
|
340
383
|
? this.checkedIcon
|
|
@@ -367,8 +410,21 @@ export default class ZnCheckbox extends ZincElement implements ZincFormControl {
|
|
|
367
410
|
: ''}
|
|
368
411
|
</span>
|
|
369
412
|
|
|
370
|
-
|
|
371
|
-
|
|
413
|
+
${hasImage
|
|
414
|
+
? html`
|
|
415
|
+
<span part="image-container" class="selection-card__image-container">
|
|
416
|
+
<slot name="image">
|
|
417
|
+
${this.src
|
|
418
|
+
? html`<img part="image" class="selection-card__image" src=${this.src} alt=${this.imageAlt}>`
|
|
419
|
+
: ''}
|
|
420
|
+
</slot>
|
|
421
|
+
</span>`
|
|
422
|
+
: ''}
|
|
423
|
+
|
|
424
|
+
<div part="label" class="checkbox__label selection-card__content">
|
|
425
|
+
<span part="card-title" class="selection-card__title">
|
|
426
|
+
<slot></slot>${hasDefaultSlot ? '' : this.cardTitle}
|
|
427
|
+
</span>
|
|
372
428
|
<div
|
|
373
429
|
aria-hidden=${hasDescription ? 'false' : 'true'}
|
|
374
430
|
class="checkbox__description"
|