@kubex/zinc 1.0.107 → 1.0.109
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/dist/custom-elements.json +85 -2
- package/dist/vscode.html-custom-data.json +2 -0
- package/dist/web-types.json +11 -1
- package/dist/zn.d.ts +11 -4
- package/dist/zn.min.css +1 -1
- package/dist/zn.min.js +200 -184
- package/docs/_includes/sidebar.njk +1 -0
- package/docs/pages/components/icon.md +5 -1
- package/docs/pages/getting-started/shimmer-text.md +81 -0
- package/package.json +1 -1
- package/scss/shared/index.scss +1 -0
- package/scss/shared/shimmer.scss +98 -0
- package/src/components/datepicker/datepicker.component.ts +6 -0
- package/src/components/datepicker/datepicker.test.ts +32 -1
- package/src/components/form-group/form-group.component.ts +7 -7
- package/src/components/icon/icon.component.ts +51 -30
- package/src/components/icon/icon.test.ts +23 -0
- package/src/components/input/input.component.ts +6 -0
- package/src/components/input/input.test.ts +34 -1
- package/src/components/markdown-editor/markdown-editor.component.ts +21 -0
- package/src/components/markdown-editor/markdown-editor.scss +24 -1
- package/src/components/select/select.component.ts +7 -0
- package/src/components/select/select.test.ts +24 -0
- package/src/components/tabs/tabs.component.ts +6 -2
- package/src/components/textarea/textarea.component.ts +8 -0
- package/src/components/textarea/textarea.test.ts +36 -3
- package/src/components/tile-property/tile-property.scss +11 -5
- package/src/utilities/sha256.ts +114 -0
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
<li><a href="/getting-started/table-examples">Table Examples</a></li>
|
|
8
8
|
<li><a href="/getting-started/example-layout">Example Layout</a></li>
|
|
9
9
|
<li><a href="/getting-started/example-page">Example Page</a></li>
|
|
10
|
+
<li><a href="/getting-started/shimmer-text">Shimmer Text</a></li>
|
|
10
11
|
</ul>
|
|
11
12
|
</li>
|
|
12
13
|
<li>
|
|
@@ -333,6 +333,8 @@ Explicit library specification:
|
|
|
333
333
|
<zn-icon src="test1@example.com" library="gravatar" size="48"></zn-icon>
|
|
334
334
|
```
|
|
335
335
|
|
|
336
|
+
Email addresses are trimmed, lowercased, and hashed with SHA-256 before the Gravatar request is made.
|
|
337
|
+
|
|
336
338
|
Using shorthand notation:
|
|
337
339
|
|
|
338
340
|
```html:preview
|
|
@@ -348,6 +350,8 @@ Libravatar is an open-source alternative to Gravatar. Use it for federated avata
|
|
|
348
350
|
<zn-icon src="user@example.com" library="libravatar" size="48" round></zn-icon>
|
|
349
351
|
```
|
|
350
352
|
|
|
353
|
+
Email addresses are trimmed, lowercased, and hashed with SHA-256 before the Libravatar request is made.
|
|
354
|
+
|
|
351
355
|
Similar to Gravatar, you can specify fallback options:
|
|
352
356
|
|
|
353
357
|
```html:preview
|
|
@@ -727,4 +731,4 @@ your HTML like this:
|
|
|
727
731
|
grid-template-columns: repeat(4, 1fr);
|
|
728
732
|
}
|
|
729
733
|
}
|
|
730
|
-
</style>
|
|
734
|
+
</style>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
meta:
|
|
3
|
+
title: Shimmer Text
|
|
4
|
+
description: A global utility class that applies an animated shimmer sweep to text, suitable for loading, processing, or "thinking" states.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Shimmer Text
|
|
8
|
+
|
|
9
|
+
`shimmer-text` is a global utility class that animates a horizontal highlight sweep across text colour. Use it to convey an in-progress state — AI generation, background processing, or any "working on it" indicator.
|
|
10
|
+
|
|
11
|
+
The class works on any element with text content, including inside Zinc components. Colour variants mirror the standard Zinc palette: `zn-primary`, `zn-accent`, `zn-info`, `zn-success`, `zn-warning`, `zn-error`.
|
|
12
|
+
|
|
13
|
+
When `prefers-reduced-motion: reduce` is set, the animation is suppressed and the text falls back to a solid colour matching the chosen variant.
|
|
14
|
+
|
|
15
|
+
## Examples
|
|
16
|
+
|
|
17
|
+
### Default
|
|
18
|
+
|
|
19
|
+
```html:preview
|
|
20
|
+
<span class="shimmer-text">Thinking…</span>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Colour Variants
|
|
24
|
+
|
|
25
|
+
```html:preview
|
|
26
|
+
<div style="display: flex; flex-direction: column; gap: 8px;">
|
|
27
|
+
<span class="shimmer-text">Default</span>
|
|
28
|
+
<span class="shimmer-text zn-primary">Primary</span>
|
|
29
|
+
<span class="shimmer-text zn-accent">Accent</span>
|
|
30
|
+
<span class="shimmer-text zn-info">Info</span>
|
|
31
|
+
<span class="shimmer-text zn-success">Success</span>
|
|
32
|
+
<span class="shimmer-text zn-warning">Warning</span>
|
|
33
|
+
<span class="shimmer-text zn-error">Error</span>
|
|
34
|
+
</div>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Headings and Larger Text
|
|
38
|
+
|
|
39
|
+
The shimmer scales with whatever `font-size` is on the element, so it can be applied to headings or inline within prose.
|
|
40
|
+
|
|
41
|
+
```html:preview
|
|
42
|
+
<h2 class="shimmer-text zn-primary" style="margin: 0;">Generating summary…</h2>
|
|
43
|
+
<p style="margin: 8px 0 0;">
|
|
44
|
+
Please wait while we <span class="shimmer-text zn-info">analyse your data</span>.
|
|
45
|
+
</p>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Inside Zinc Components
|
|
49
|
+
|
|
50
|
+
Because `shimmer-text` is a plain class, it composes with any Zinc component that renders slotted text.
|
|
51
|
+
|
|
52
|
+
```html:preview
|
|
53
|
+
<zn-chip class="shimmer-text zn-accent">Processing</zn-chip>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Tuning the Sweep
|
|
57
|
+
|
|
58
|
+
The effect mirrors the Claude CLI's character-level shimmer: a narrow, sharper highlight slides across text rather than a soft gradient wash. The class exposes these CSS custom properties — all sized in `ch` so they scale with the current font size.
|
|
59
|
+
|
|
60
|
+
| Variable | Default | Purpose |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `--shimmer-base` | `rgb(var(--zn-text))` | Resting text colour. |
|
|
63
|
+
| `--shimmer-highlight` | `rgb(var(--zn-text-heading))` | Colour of the spotlight. |
|
|
64
|
+
| `--shimmer-core` | `0.5ch` | Half-width of the solid-colour core of the spotlight. |
|
|
65
|
+
| `--shimmer-spread` | `1.5ch` | Half-width of the full highlight including its fade-out edges. |
|
|
66
|
+
| `--shimmer-duration` | `2s` | Time for one full sweep cycle. |
|
|
67
|
+
|
|
68
|
+
```html:preview
|
|
69
|
+
<div style="display: flex; flex-direction: column; gap: 6px;">
|
|
70
|
+
<span class="shimmer-text">Default sweep</span>
|
|
71
|
+
<span class="shimmer-text" style="--shimmer-duration: 1s;">Faster sweep</span>
|
|
72
|
+
<span class="shimmer-text" style="--shimmer-duration: 4s;">Slower, gentler sweep</span>
|
|
73
|
+
<span class="shimmer-text" style="--shimmer-core: 1ch; --shimmer-spread: 3ch;">Wider spotlight</span>
|
|
74
|
+
</div>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The Claude CLI scales its sweep with message length (50ms per character plus 10 characters of offscreen pause on each side). To match that feel in CSS, set `--shimmer-duration` to roughly `(messageCharacters + 20) × 50ms`.
|
|
78
|
+
|
|
79
|
+
### Reduced Motion
|
|
80
|
+
|
|
81
|
+
The animation is wrapped in `@media (prefers-reduced-motion: no-preference)`. Users who have requested reduced motion see the static colour variant instead — no configuration required.
|
package/package.json
CHANGED
package/scss/shared/index.scss
CHANGED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
|
|
2
|
+
@keyframes shimmer-text-sweep {
|
|
3
|
+
0% {
|
|
4
|
+
background-position: 100% center;
|
|
5
|
+
}
|
|
6
|
+
100% {
|
|
7
|
+
background-position: 0% center;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
12
|
+
.shimmer-text {
|
|
13
|
+
--shimmer-base: rgb(var(--zn-text));
|
|
14
|
+
--shimmer-highlight: rgb(var(--zn-text-heading));
|
|
15
|
+
--shimmer-core: 0.5ch;
|
|
16
|
+
--shimmer-spread: 1.5ch;
|
|
17
|
+
--shimmer-duration: 1.5s;
|
|
18
|
+
|
|
19
|
+
display: inline-block;
|
|
20
|
+
width: fit-content;
|
|
21
|
+
max-width: 100%;
|
|
22
|
+
background-image:
|
|
23
|
+
linear-gradient(
|
|
24
|
+
90deg,
|
|
25
|
+
transparent calc(50% - var(--shimmer-spread)),
|
|
26
|
+
var(--shimmer-highlight) calc(50% - var(--shimmer-core)),
|
|
27
|
+
var(--shimmer-highlight) calc(50% + var(--shimmer-core)),
|
|
28
|
+
transparent calc(50% + var(--shimmer-spread))
|
|
29
|
+
),
|
|
30
|
+
linear-gradient(var(--shimmer-base), var(--shimmer-base));
|
|
31
|
+
background-size: 250% 100%, auto;
|
|
32
|
+
background-clip: text;
|
|
33
|
+
-webkit-background-clip: text;
|
|
34
|
+
-webkit-text-fill-color: transparent !important;
|
|
35
|
+
color: transparent !important;
|
|
36
|
+
animation: var(--shimmer-duration) linear infinite shimmer-text-sweep;
|
|
37
|
+
|
|
38
|
+
&.zn-error {
|
|
39
|
+
--shimmer-base: rgb(var(--zn-color-error));
|
|
40
|
+
--shimmer-highlight: color-mix(in srgb, rgb(var(--zn-color-error)) 55%, white);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
&.zn-success {
|
|
44
|
+
--shimmer-base: rgb(var(--zn-color-success));
|
|
45
|
+
--shimmer-highlight: color-mix(in srgb, rgb(var(--zn-color-success)) 55%, white);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
&.zn-info {
|
|
49
|
+
--shimmer-base: rgb(var(--zn-color-info));
|
|
50
|
+
--shimmer-highlight: color-mix(in srgb, rgb(var(--zn-color-info)) 55%, white);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
&.zn-warning {
|
|
54
|
+
--shimmer-base: rgb(var(--zn-color-warning));
|
|
55
|
+
--shimmer-highlight: color-mix(in srgb, rgb(var(--zn-color-warning)) 55%, white);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
&.zn-primary {
|
|
59
|
+
--shimmer-base: rgb(var(--zn-primary));
|
|
60
|
+
--shimmer-highlight: color-mix(in srgb, rgb(var(--zn-primary)) 55%, white);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
&.zn-accent {
|
|
64
|
+
--shimmer-base: rgb(var(--zn-accent));
|
|
65
|
+
--shimmer-highlight: color-mix(in srgb, rgb(var(--zn-accent)) 55%, white);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@media (prefers-reduced-motion: reduce) {
|
|
71
|
+
.shimmer-text {
|
|
72
|
+
color: rgb(var(--zn-text));
|
|
73
|
+
|
|
74
|
+
&.zn-error {
|
|
75
|
+
color: rgb(var(--zn-color-error)) !important;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
&.zn-success {
|
|
79
|
+
color: rgb(var(--zn-color-success)) !important;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
&.zn-info {
|
|
83
|
+
color: rgb(var(--zn-color-info)) !important;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
&.zn-warning {
|
|
87
|
+
color: rgb(var(--zn-color-warning)) !important;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
&.zn-primary {
|
|
91
|
+
color: rgb(var(--zn-primary)) !important;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
&.zn-accent {
|
|
95
|
+
color: rgb(var(--zn-accent)) !important;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -321,6 +321,12 @@ export default class ZnDatepicker extends ZincElement implements ZincFormControl
|
|
|
321
321
|
return;
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
if (event.key === 'Escape') {
|
|
325
|
+
this.input.blur();
|
|
326
|
+
event.stopPropagation();
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
324
330
|
// Allow navigation and control keys
|
|
325
331
|
const allowedKeys = [
|
|
326
332
|
'Backspace', 'Delete', 'Tab', 'Escape', 'Enter',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import '../../../dist/zn.min.js';
|
|
2
|
-
import {
|
|
2
|
+
import {expect, fixture, html} from '@open-wc/testing';
|
|
3
|
+
import type ZnDatepicker from './datepicker.component';
|
|
3
4
|
|
|
4
5
|
describe('<zn-datepicker>', () => {
|
|
5
6
|
it('should render a component', async () => {
|
|
@@ -7,4 +8,34 @@ describe('<zn-datepicker>', () => {
|
|
|
7
8
|
|
|
8
9
|
expect(el).to.exist;
|
|
9
10
|
});
|
|
11
|
+
|
|
12
|
+
describe('Escape key', () => {
|
|
13
|
+
it('blurs the inner input when focused and the calendar is closed', async () => {
|
|
14
|
+
const el = await fixture<ZnDatepicker>(html`<zn-datepicker></zn-datepicker>`);
|
|
15
|
+
await el.updateComplete;
|
|
16
|
+
|
|
17
|
+
// Focus shows AirDatepicker's calendar (showEvent: 'focus'). Per spec, Escape
|
|
18
|
+
// while calendar is open is handled by AirDatepicker; our handler engages once
|
|
19
|
+
// the calendar is closed. Hide it explicitly to simulate the calendar-closed
|
|
20
|
+
// state, then press Escape.
|
|
21
|
+
el.focus();
|
|
22
|
+
await el.updateComplete;
|
|
23
|
+
const dp = (el as unknown as { _instance?: { hide: () => void; visible: boolean } })._instance;
|
|
24
|
+
dp?.hide();
|
|
25
|
+
await el.updateComplete;
|
|
26
|
+
el.input.focus();
|
|
27
|
+
await el.updateComplete;
|
|
28
|
+
expect(el.shadowRoot!.activeElement).to.equal(el.input);
|
|
29
|
+
|
|
30
|
+
el.input.dispatchEvent(new KeyboardEvent('keydown', {
|
|
31
|
+
key: 'Escape',
|
|
32
|
+
bubbles: true,
|
|
33
|
+
composed: true,
|
|
34
|
+
cancelable: true
|
|
35
|
+
}));
|
|
36
|
+
await el.updateComplete;
|
|
37
|
+
|
|
38
|
+
expect(el.shadowRoot!.activeElement, 'inner input should be blurred').to.not.equal(el.input);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
10
41
|
});
|
|
@@ -74,14 +74,14 @@ export default class ZnFormGroup extends ZincElement {
|
|
|
74
74
|
</zn-tooltip>`
|
|
75
75
|
: ''}
|
|
76
76
|
</label>
|
|
77
|
+
</div>` : html``}
|
|
77
78
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
</div>` : html``}
|
|
79
|
+
${hasHelpText ? html`
|
|
80
|
+
<div
|
|
81
|
+
part="form-control-help-text"
|
|
82
|
+
id="help-text"
|
|
83
|
+
class="form-control__help-text">
|
|
84
|
+
<slot name="help-text">${this.helpText}</slot>
|
|
85
85
|
</div>` : html``}
|
|
86
86
|
|
|
87
87
|
<div part="form-control-input" class="form-control-input">
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {choose} from 'lit/directives/choose.js';
|
|
2
2
|
import {classMap} from "lit/directives/class-map.js";
|
|
3
3
|
import {type CSSResultGroup, html, render, unsafeCSS} from 'lit';
|
|
4
|
-
import {md5} from '../../utilities/md5';
|
|
5
4
|
import {property} from 'lit/decorators.js';
|
|
5
|
+
import {sha256} from '../../utilities/sha256';
|
|
6
6
|
import {styleMap} from "lit/directives/style-map.js";
|
|
7
7
|
import ZincElement from '../../internal/zinc-element';
|
|
8
8
|
|
|
@@ -30,7 +30,7 @@ export type IconColor =
|
|
|
30
30
|
| "violet"
|
|
31
31
|
| "pink"
|
|
32
32
|
| "grey"
|
|
33
|
-
| (string &
|
|
33
|
+
| (string & Record<never, never>);
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* @summary Short summary of the component's intended use.
|
|
@@ -127,40 +127,37 @@ export default class ZnIcon extends ZincElement {
|
|
|
127
127
|
connectedCallback() {
|
|
128
128
|
super.connectedCallback();
|
|
129
129
|
|
|
130
|
+
let hashFragment = '';
|
|
130
131
|
|
|
131
132
|
if (this.src && this.src.includes('#')) {
|
|
132
|
-
const split = this.src.split('#');
|
|
133
|
+
const split = this.src.split('#', 2);
|
|
133
134
|
this.src = split[0];
|
|
134
|
-
|
|
135
|
-
attributes.forEach(attr => {
|
|
136
|
-
if (attr === "round") {
|
|
137
|
-
this.round = true;
|
|
138
|
-
}
|
|
139
|
-
if (attr === "tile") {
|
|
140
|
-
this.tile = true;
|
|
141
|
-
}
|
|
142
|
-
if (attr === "depth") {
|
|
143
|
-
this.depth = true;
|
|
144
|
-
}
|
|
145
|
-
});
|
|
135
|
+
hashFragment = split[1] ?? '';
|
|
146
136
|
}
|
|
147
137
|
|
|
148
138
|
if (this.src && this.src.includes('@')) {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
139
|
+
const atIndex = this.src.lastIndexOf('@');
|
|
140
|
+
const libraryOrDomain = this.src.slice(atIndex + 1);
|
|
141
|
+
|
|
142
|
+
if (libraryOrDomain.includes('.')) {
|
|
143
|
+
this.library = this.library ?? "gravatar";
|
|
144
|
+
} else if ((this.library === undefined) && libraryOrDomain !== "") {
|
|
153
145
|
// if split[1] is a valid library name, set it
|
|
154
|
-
this.library = this.convertToLibrary(
|
|
155
|
-
this.src =
|
|
146
|
+
this.library = this.convertToLibrary(libraryOrDomain);
|
|
147
|
+
this.src = this.src.slice(0, atIndex);
|
|
156
148
|
}
|
|
157
149
|
|
|
158
150
|
if (this.library === "gravatar" || this.library === "libravatar") {
|
|
159
|
-
this.
|
|
160
|
-
this.src =
|
|
151
|
+
this.applyHashFragment(hashFragment);
|
|
152
|
+
this.src = sha256(this.normalizeRavatarEmail(this.src));
|
|
153
|
+
} else {
|
|
154
|
+
this.applyHashFragment(hashFragment);
|
|
161
155
|
}
|
|
162
156
|
} else if (!this.library && this.src && !this.src.includes('/')) {
|
|
157
|
+
this.applyHashFragment(hashFragment);
|
|
163
158
|
this.library = this.defaultLibrary;
|
|
159
|
+
} else {
|
|
160
|
+
this.applyHashFragment(hashFragment);
|
|
164
161
|
}
|
|
165
162
|
|
|
166
163
|
// load the material icons font if the library is set to material
|
|
@@ -174,18 +171,42 @@ export default class ZnIcon extends ZincElement {
|
|
|
174
171
|
<link
|
|
175
172
|
href="https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined|Material+Icons|Material+Icons+Round|Material+Icons+Sharp|Material+Icons+Two+Tone|Material+Icons+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"
|
|
176
173
|
rel="stylesheet">`, document.head);
|
|
177
|
-
|
|
178
|
-
this.ravatarOptions();
|
|
179
174
|
}
|
|
180
175
|
|
|
181
|
-
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
176
|
+
private applyHashFragment(hashFragment: string) {
|
|
177
|
+
if (!hashFragment) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const attributes = hashFragment.split(',');
|
|
182
|
+
|
|
183
|
+
attributes.forEach(attr => {
|
|
184
|
+
if (attr === 'round') {
|
|
185
|
+
this.round = true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (attr === 'tile') {
|
|
189
|
+
this.tile = true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (attr === 'depth') {
|
|
193
|
+
this.depth = true;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
if ((this.library === 'gravatar' || this.library === 'libravatar')) {
|
|
198
|
+
const defaultOption = attributes.find(attr => !['round', 'tile', 'depth'].includes(attr));
|
|
199
|
+
|
|
200
|
+
if (defaultOption) {
|
|
201
|
+
this.gravatarOptions = `&d=${defaultOption}`;
|
|
202
|
+
}
|
|
186
203
|
}
|
|
187
204
|
}
|
|
188
205
|
|
|
206
|
+
private normalizeRavatarEmail(email: string) {
|
|
207
|
+
return email.trim().toLowerCase();
|
|
208
|
+
}
|
|
209
|
+
|
|
189
210
|
render() {
|
|
190
211
|
return html`
|
|
191
212
|
<div class="icon-wrapper">
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import '../../../dist/zn.min.js';
|
|
2
2
|
import { expect, fixture, html } from '@open-wc/testing';
|
|
3
|
+
import type ZnIcon from './icon.component';
|
|
3
4
|
|
|
4
5
|
describe('<zn-icon>', () => {
|
|
5
6
|
it('should render a component', async () => {
|
|
@@ -7,4 +8,26 @@ describe('<zn-icon>', () => {
|
|
|
7
8
|
|
|
8
9
|
expect(el).to.exist;
|
|
9
10
|
});
|
|
11
|
+
|
|
12
|
+
it('should build Gravatar URLs with a normalized SHA-256 hash', async () => {
|
|
13
|
+
const el = await fixture<ZnIcon>(html`
|
|
14
|
+
<zn-icon src="MyEmailAddress@example.com " library="gravatar" size="48"></zn-icon>
|
|
15
|
+
`);
|
|
16
|
+
|
|
17
|
+
const image = el.shadowRoot?.querySelector('img');
|
|
18
|
+
expect(image?.getAttribute('src')).to.equal(
|
|
19
|
+
'https://www.gravatar.com/avatar/84059b07d4be67b806386c0aad8070a23f18836bbaae342275dc0a83414c32ee?s=48'
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should pass fallback options through to Libravatar', async () => {
|
|
24
|
+
const el = await fixture<ZnIcon>(html`
|
|
25
|
+
<zn-icon src="nonexistent@example.com#identicon" library="libravatar" size="48"></zn-icon>
|
|
26
|
+
`);
|
|
27
|
+
|
|
28
|
+
const image = el.shadowRoot?.querySelector('img');
|
|
29
|
+
expect(image?.getAttribute('src')).to.equal(
|
|
30
|
+
'https://seccdn.libravatar.org/avatar/04e8703fcfb60849fb5c596abc8f3d7547c7e0099c3806a3ffec77a95dc02e25?s=48&d=identicon'
|
|
31
|
+
);
|
|
32
|
+
});
|
|
10
33
|
});
|
|
@@ -547,6 +547,12 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
|
|
|
547
547
|
}
|
|
548
548
|
|
|
549
549
|
private handleKeyDown(event: KeyboardEvent) {
|
|
550
|
+
if (event.key === 'Escape') {
|
|
551
|
+
this.input.blur();
|
|
552
|
+
event.stopPropagation();
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
550
556
|
const hasModifier = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
|
|
551
557
|
|
|
552
558
|
// Pressing enter when focused on the input should submit the form like a native input, be we wait a tick before
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import '../../../dist/zn.min.js';
|
|
2
|
-
import {
|
|
2
|
+
import {expect, fixture, html} from '@open-wc/testing';
|
|
3
|
+
import type ZnInput from './input.component';
|
|
3
4
|
|
|
4
5
|
describe('<zn-input>', () => {
|
|
5
6
|
it('should render a component', async () => {
|
|
@@ -7,4 +8,36 @@ describe('<zn-input>', () => {
|
|
|
7
8
|
|
|
8
9
|
expect(el).to.exist;
|
|
9
10
|
});
|
|
11
|
+
|
|
12
|
+
describe('Escape key', () => {
|
|
13
|
+
it('blurs the inner input and stops propagation when focused', async () => {
|
|
14
|
+
const wrapper = await fixture<HTMLDivElement>(html`
|
|
15
|
+
<div>
|
|
16
|
+
<zn-input></zn-input>
|
|
17
|
+
</div>
|
|
18
|
+
`);
|
|
19
|
+
const el = wrapper.querySelector('zn-input') as ZnInput;
|
|
20
|
+
await el.updateComplete;
|
|
21
|
+
|
|
22
|
+
let wrapperEscapes = 0;
|
|
23
|
+
wrapper.addEventListener('keydown', (e) => {
|
|
24
|
+
if ((e as KeyboardEvent).key === 'Escape') wrapperEscapes++;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
el.focus();
|
|
28
|
+
await el.updateComplete;
|
|
29
|
+
expect(el.shadowRoot!.activeElement).to.equal(el.input);
|
|
30
|
+
|
|
31
|
+
el.input.dispatchEvent(new KeyboardEvent('keydown', {
|
|
32
|
+
key: 'Escape',
|
|
33
|
+
bubbles: true,
|
|
34
|
+
composed: true,
|
|
35
|
+
cancelable: true
|
|
36
|
+
}));
|
|
37
|
+
await el.updateComplete;
|
|
38
|
+
|
|
39
|
+
expect(el.shadowRoot!.activeElement, 'inner input should be blurred').to.not.equal(el.input);
|
|
40
|
+
expect(wrapperEscapes, 'wrapper should not see Escape on press 1').to.equal(0);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
10
43
|
});
|
|
@@ -269,6 +269,8 @@ export default class ZnMarkdownEditor extends ZnPanel implements ZincFormControl
|
|
|
269
269
|
'panel--cosmic': this.cosmic,
|
|
270
270
|
'panel--shadow': this.shadow,
|
|
271
271
|
})}>
|
|
272
|
+
|
|
273
|
+
|
|
272
274
|
<div class="panel__inner">
|
|
273
275
|
${hasHeader ? html`
|
|
274
276
|
<zn-header class=${classMap({
|
|
@@ -297,6 +299,7 @@ export default class ZnMarkdownEditor extends ZnPanel implements ZincFormControl
|
|
|
297
299
|
</label>` : ''}
|
|
298
300
|
|
|
299
301
|
<div part="toolbar" class="markdown-editor__toolbar">
|
|
302
|
+
${this.markdownHelperBtn()}
|
|
300
303
|
<zn-button-group class="markdown-editor__view-toggle" @click=${this.handleViewToggle}>
|
|
301
304
|
${VIEW_MODES.map(v => this.renderViewButton(v.mode, v.icon, v.label))}
|
|
302
305
|
</zn-button-group>
|
|
@@ -351,6 +354,21 @@ export default class ZnMarkdownEditor extends ZnPanel implements ZincFormControl
|
|
|
351
354
|
`;
|
|
352
355
|
}
|
|
353
356
|
|
|
357
|
+
markdownHelperBtn() {
|
|
358
|
+
return html`
|
|
359
|
+
<a
|
|
360
|
+
class="markdown-editor__markdown-link"
|
|
361
|
+
href="https://www.markdownguide.org/cheat-sheet"
|
|
362
|
+
target="_blank"
|
|
363
|
+
rel="noopener noreferrer"
|
|
364
|
+
title="Markdown is supported"
|
|
365
|
+
>
|
|
366
|
+
<zn-icon src="markdown"></zn-icon>
|
|
367
|
+
<span>Markdown is supported</span>
|
|
368
|
+
</a>
|
|
369
|
+
`;
|
|
370
|
+
}
|
|
371
|
+
|
|
354
372
|
private renderViewButton(mode: ViewMode, icon: string, label: string) {
|
|
355
373
|
const isActive = this.viewMode === mode;
|
|
356
374
|
return html`
|
|
@@ -372,4 +390,7 @@ export default class ZnMarkdownEditor extends ZnPanel implements ZincFormControl
|
|
|
372
390
|
></zn-button>
|
|
373
391
|
`;
|
|
374
392
|
}
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
|
|
375
396
|
}
|
|
@@ -44,9 +44,32 @@
|
|
|
44
44
|
|
|
45
45
|
.markdown-editor__toolbar {
|
|
46
46
|
display: flex;
|
|
47
|
-
align-items:
|
|
47
|
+
align-items: end;
|
|
48
48
|
gap: var(--zn-spacing-small);
|
|
49
49
|
justify-content: flex-end;
|
|
50
|
+
flex-wrap: wrap;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.markdown-editor__markdown-link {
|
|
54
|
+
display: inline-flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
gap: var(--zn-spacing-x-small);
|
|
57
|
+
margin-right: auto;
|
|
58
|
+
font-size: var(--zn-input-label-font-size-small);
|
|
59
|
+
line-height: 1;
|
|
60
|
+
color: var(--zn-color-neutral-500, #656d76);
|
|
61
|
+
text-decoration: none;
|
|
62
|
+
cursor: pointer;
|
|
63
|
+
transition: color 0.15s ease;
|
|
64
|
+
padding-left: var(--zn-spacing-x-small);
|
|
65
|
+
|
|
66
|
+
&:hover {
|
|
67
|
+
color: var(--zn-color-primary-600, #0969da);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
span {
|
|
71
|
+
white-space: nowrap;
|
|
72
|
+
}
|
|
50
73
|
}
|
|
51
74
|
|
|
52
75
|
.markdown-editor__view-toggle {
|
|
@@ -484,6 +484,13 @@ export default class ZnSelect extends ZincElement implements ZincFormControl {
|
|
|
484
484
|
this.displayInput.focus({preventScroll: true});
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
+
// Blur the display input when Escape is pressed and the dropdown is closed,
|
|
488
|
+
// so a second Escape press can bubble (e.g. close a containing dialog).
|
|
489
|
+
if (event.key === 'Escape' && !this.open) {
|
|
490
|
+
event.stopPropagation();
|
|
491
|
+
this.displayInput.blur();
|
|
492
|
+
}
|
|
493
|
+
|
|
487
494
|
// Handle enter and space. When pressing space, we allow for type to select behaviors so if there's anything in the
|
|
488
495
|
// buffer we _don't_ close it. When search is enabled, space should type into the input, not toggle.
|
|
489
496
|
if (event.key === 'Enter' || (event.key === ' ' && !this.search && this.typeToSelectString === '')) {
|
|
@@ -152,4 +152,28 @@ describe('<zn-select>', () => {
|
|
|
152
152
|
expect(visibleOptions[0].value).to.equal('apple');
|
|
153
153
|
});
|
|
154
154
|
});
|
|
155
|
+
|
|
156
|
+
describe('Escape key', () => {
|
|
157
|
+
it('blurs the display input when dropdown is closed and Escape is pressed', async () => {
|
|
158
|
+
const el = await fixture<ZnSelect>(html`<zn-select></zn-select>`);
|
|
159
|
+
await el.updateComplete;
|
|
160
|
+
|
|
161
|
+
const displayInput = el.shadowRoot!.querySelector<HTMLInputElement>('.select__display-input')!;
|
|
162
|
+
el.focus();
|
|
163
|
+
await el.updateComplete;
|
|
164
|
+
expect(el.shadowRoot!.activeElement).to.equal(displayInput);
|
|
165
|
+
expect(el.open).to.be.false;
|
|
166
|
+
|
|
167
|
+
displayInput.dispatchEvent(new KeyboardEvent('keydown', {
|
|
168
|
+
key: 'Escape',
|
|
169
|
+
bubbles: true,
|
|
170
|
+
composed: true,
|
|
171
|
+
cancelable: true
|
|
172
|
+
}));
|
|
173
|
+
await el.updateComplete;
|
|
174
|
+
|
|
175
|
+
expect(el.shadowRoot!.activeElement, 'display input should be blurred').to.not.equal(displayInput);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
});
|
|
155
179
|
});
|