@kubex/zinc 1.1.81 → 1.1.83
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 +95 -2
- package/dist/vscode.html-custom-data.json +10 -0
- package/dist/web-types.json +21 -1
- package/dist/zn.d.ts +23 -0
- package/dist/zn.min.js +230 -224
- package/docs/pages/components/cols.md +43 -0
- package/docs/pages/components/translations.md +22 -4
- package/package.json +1 -1
- package/src/components/cols/cols.component.ts +79 -0
- package/src/components/cols/cols.scss +45 -0
- package/src/components/cols/cols.test.ts +131 -3
- package/src/components/translations/translations.component.ts +16 -1
- package/src/components/translations/translations.test.ts +42 -0
|
@@ -3,6 +3,7 @@ meta:
|
|
|
3
3
|
title: Cols
|
|
4
4
|
description: A flexible column layout component that distributes children across multiple columns with configurable layouts, gaps, borders, and padding options.
|
|
5
5
|
layout: component
|
|
6
|
+
fullWidth: true
|
|
6
7
|
---
|
|
7
8
|
|
|
8
9
|
```html:preview
|
|
@@ -74,6 +75,48 @@ The layout pattern repeats for additional children. With "2,1", items cycle thro
|
|
|
74
75
|
</zn-cols>
|
|
75
76
|
```
|
|
76
77
|
|
|
78
|
+
### Stacking Order
|
|
79
|
+
|
|
80
|
+
Use `stack-at` to name the container width the columns collapse to a single column below, as one of
|
|
81
|
+
the container sizes (`sm`, `smp`, `ph`, `md`, `lg`, `hd`, `3k`, `4k`). Children can then declare a
|
|
82
|
+
`stack-order` to say where they belong in that single column, which lets a sidebar sit second in the
|
|
83
|
+
markup but move to the top once stacked.
|
|
84
|
+
|
|
85
|
+
`stack-order` takes `first`/`high` (before unordered children), `last`/`low` (after them), or any
|
|
86
|
+
integer for finer control. It has no effect while the columns are side by side, so the reading order
|
|
87
|
+
of your markup is what wide screens get.
|
|
88
|
+
|
|
89
|
+
```html:preview
|
|
90
|
+
<zn-cols layout="2,1" stack-at="md">
|
|
91
|
+
<zn-panel>Main content — first in the markup, second when stacked</zn-panel>
|
|
92
|
+
<zn-panel stack-order="first">Sidebar — second in the markup, first when stacked</zn-panel>
|
|
93
|
+
</zn-cols>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Splitting a Column by Priority
|
|
97
|
+
|
|
98
|
+
Add `stack-split` to a column to break it apart when stacked. Its children stay together as one
|
|
99
|
+
column on wide screens, then each becomes a column in its own right and takes its own `stack-order`,
|
|
100
|
+
so high priority content can move above the main column while low priority content drops below it.
|
|
101
|
+
|
|
102
|
+
```html:preview
|
|
103
|
+
<zn-cols layout="2,1" stack-at="md">
|
|
104
|
+
<zn-panel>Databases — the main column</zn-panel>
|
|
105
|
+
<div stack-split>
|
|
106
|
+
<zn-panel stack-order="high">Usage — above the main column when stacked</zn-panel>
|
|
107
|
+
<zn-panel stack-order="low">Tutorials — below the main column when stacked</zn-panel>
|
|
108
|
+
</div>
|
|
109
|
+
</zn-cols>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Keep `stack-split` on a plain wrapper element rather than a component: `zn-cols` lays it out as a
|
|
113
|
+
column (`display: flex` in a column, with the standard gap) and then removes its box entirely while
|
|
114
|
+
stacked, so any styling of its own would disappear along with it.
|
|
115
|
+
|
|
116
|
+
`stack-at` defaults to `lg` when any child declares `stack-order` or `stack-split`. Set it explicitly
|
|
117
|
+
at or above the width where your columns would otherwise wrap on their own, so that reordering is
|
|
118
|
+
already in effect by the time the layout breaks.
|
|
119
|
+
|
|
77
120
|
### With Borders
|
|
78
121
|
|
|
79
122
|
Use the `border` attribute to add borders around columns.
|
|
@@ -323,6 +323,22 @@ A complete example showing product content management with translations.
|
|
|
323
323
|
</div>
|
|
324
324
|
```
|
|
325
325
|
|
|
326
|
+
### Help Text
|
|
327
|
+
|
|
328
|
+
Use the `help-text` attribute to describe how the field should be filled in. It sits below the field and applies to
|
|
329
|
+
every language, so it is the place to explain a convention the translator needs to follow. For help text containing
|
|
330
|
+
HTML, use the `help-text` slot instead.
|
|
331
|
+
|
|
332
|
+
```html:preview
|
|
333
|
+
<zn-translations
|
|
334
|
+
label="Confirmation headline"
|
|
335
|
+
help-text="Keep this under 60 characters so it does not wrap on mobile."></zn-translations>
|
|
336
|
+
<br />
|
|
337
|
+
<zn-translations label="Footer">
|
|
338
|
+
<div slot="help-text">Shown on <strong>every</strong> page of the checkout.</div>
|
|
339
|
+
</zn-translations>
|
|
340
|
+
```
|
|
341
|
+
|
|
326
342
|
### Slash Menu Quick Insertions
|
|
327
343
|
|
|
328
344
|
{% raw %}
|
|
@@ -360,6 +376,7 @@ way `Enter` otherwise would.
|
|
|
360
376
|
| `name` | `string` | `''` | Form field name for submission |
|
|
361
377
|
| `value` | `string` | `'{"en":""}'` | JSON string of translations |
|
|
362
378
|
| `label` | `string` | `''` | Label displayed above the component |
|
|
379
|
+
| `help-text` | `string` | `''` | Text shown below the field, describing how to fill it in |
|
|
363
380
|
| `disabled` | `boolean` | `false` | Disables editing of all translations |
|
|
364
381
|
| `required` | `boolean` | `false` | Makes the field required for form validation |
|
|
365
382
|
| `flush` | `boolean` | `false` | Removes padding for compact layout |
|
|
@@ -379,10 +396,11 @@ way `Enter` otherwise would.
|
|
|
379
396
|
|
|
380
397
|
## Slots
|
|
381
398
|
|
|
382
|
-
| Slot
|
|
383
|
-
|
|
384
|
-
| `label`
|
|
385
|
-
| `expand`
|
|
399
|
+
| Slot | Description |
|
|
400
|
+
|-------------|------------------------------------------------------------------|
|
|
401
|
+
| `label` | Alternative to the `label` attribute for rich HTML content |
|
|
402
|
+
| `expand` | Action button displayed in the navbar (e.g., translate button) |
|
|
403
|
+
| `help-text` | Alternative to the `help-text` attribute for rich HTML content |
|
|
386
404
|
|
|
387
405
|
## Methods
|
|
388
406
|
|
package/package.json
CHANGED
|
@@ -5,6 +5,18 @@ import ZincElement from '../../internal/zinc-element';
|
|
|
5
5
|
|
|
6
6
|
import styles from './cols.scss';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Priority keywords accepted by the `stack-order` attribute on children.
|
|
10
|
+
*/
|
|
11
|
+
const STACK_ORDER_KEYWORDS: Record<string, number> = {
|
|
12
|
+
first: -1,
|
|
13
|
+
high: -1,
|
|
14
|
+
last: 1,
|
|
15
|
+
low: 1,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const DEFAULT_STACK_AT = 'lg';
|
|
19
|
+
|
|
8
20
|
/**
|
|
9
21
|
* @summary Short summary of the component's intended use.
|
|
10
22
|
* @documentation https://zinc.style/components/columns
|
|
@@ -27,6 +39,13 @@ export default class ZnCols extends ZincElement {
|
|
|
27
39
|
|
|
28
40
|
@property({reflect: true, attribute: 'layout'}) layout: string = '';
|
|
29
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Container width the columns collapse to a single column below, as one of the named zinc
|
|
44
|
+
* container sizes (sm, smp, ph, md, lg, hd, 3k, 4k). Defaults to `lg` when a child declares
|
|
45
|
+
* `stack-order` or `stack-split`, otherwise the columns never explicitly stack.
|
|
46
|
+
*/
|
|
47
|
+
@property({attribute: 'stack-at', reflect: true}) stackAt: string = '';
|
|
48
|
+
|
|
30
49
|
@property({attribute: 'mc', type: Number, reflect: true}) maxColumns: number = 0;
|
|
31
50
|
|
|
32
51
|
@property({attribute: 'no-gap', type: Boolean}) noGap: boolean = false;
|
|
@@ -41,6 +60,52 @@ export default class ZnCols extends ZincElement {
|
|
|
41
60
|
|
|
42
61
|
@property({attribute: 'pad-y', type: Boolean}) padY: boolean;
|
|
43
62
|
|
|
63
|
+
// Column classes and stack ordering are written onto our children, so a re-render is needed
|
|
64
|
+
// whenever they change. Registered on the host and on each [stack-split] column, rather than
|
|
65
|
+
// the whole subtree, to avoid reacting to unrelated content updates deeper in a column.
|
|
66
|
+
private readonly childObserver: MutationObserver = new MutationObserver(() => this.requestUpdate());
|
|
67
|
+
|
|
68
|
+
connectedCallback() {
|
|
69
|
+
super.connectedCallback();
|
|
70
|
+
this.childObserver.observe(this, {childList: true});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
disconnectedCallback() {
|
|
74
|
+
super.disconnectedCallback();
|
|
75
|
+
this.childObserver.disconnect();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Reads the `stack-order` attribute of an element, returning null when it does not declare one.
|
|
80
|
+
*/
|
|
81
|
+
private stackOrder(element: Element): number | null {
|
|
82
|
+
const raw = element.getAttribute('stack-order');
|
|
83
|
+
if (raw === null) return null;
|
|
84
|
+
|
|
85
|
+
const keyword = raw.trim().toLowerCase();
|
|
86
|
+
if (keyword in STACK_ORDER_KEYWORDS) return STACK_ORDER_KEYWORDS[keyword];
|
|
87
|
+
|
|
88
|
+
const order = parseInt(keyword, 10);
|
|
89
|
+
return isNaN(order) ? null : order;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Applies the declared stack order to an element. `--zn-stacked` is 0 until the container
|
|
94
|
+
* query in our stylesheet flips it to 1, so ordering only kicks in once stacked.
|
|
95
|
+
*/
|
|
96
|
+
private applyStackOrder(element: HTMLElement, promoted: boolean): number | null {
|
|
97
|
+
const order = this.stackOrder(element);
|
|
98
|
+
element.style.order = order === null ? '' : `calc(var(--zn-stacked, 0) * ${order})`;
|
|
99
|
+
|
|
100
|
+
// Children of a [stack-split] column become columns themselves when stacked, at which point
|
|
101
|
+
// they need a basis to fill the row with. --zn-stack-basis is only set while stacked.
|
|
102
|
+
if (promoted) {
|
|
103
|
+
element.style.flexBasis = 'var(--zn-stack-basis, auto)';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return order;
|
|
107
|
+
}
|
|
108
|
+
|
|
44
109
|
render() {
|
|
45
110
|
const layout: number[] = this.layout.split(/[\s,]+/).map((a) => parseInt(a)).filter((item) => !!item);
|
|
46
111
|
|
|
@@ -58,6 +123,8 @@ export default class ZnCols extends ZincElement {
|
|
|
58
123
|
const colsPerRow = layout.length;
|
|
59
124
|
const lastRowStart = children.length - (children.length % colsPerRow || colsPerRow);
|
|
60
125
|
|
|
126
|
+
let stacks = false;
|
|
127
|
+
|
|
61
128
|
children.forEach((element, index) => {
|
|
62
129
|
const classes = element.className.split(' ').filter((c) => !c.startsWith(prefix));
|
|
63
130
|
element.className = classes.join(' ');
|
|
@@ -71,8 +138,20 @@ export default class ZnCols extends ZincElement {
|
|
|
71
138
|
} else {
|
|
72
139
|
element.style.overflow = '';
|
|
73
140
|
}
|
|
141
|
+
|
|
142
|
+
stacks = this.applyStackOrder(element, false) !== null || stacks;
|
|
143
|
+
|
|
144
|
+
if(element.hasAttribute('stack-split')) {
|
|
145
|
+
stacks = true;
|
|
146
|
+
this.childObserver.observe(element, {childList: true});
|
|
147
|
+
Array.from(element.children).forEach((child) => this.applyStackOrder(child as HTMLElement, true));
|
|
148
|
+
}
|
|
74
149
|
});
|
|
75
150
|
|
|
151
|
+
if(stacks && !this.stackAt) {
|
|
152
|
+
this.stackAt = DEFAULT_STACK_AT;
|
|
153
|
+
}
|
|
154
|
+
|
|
76
155
|
return html`
|
|
77
156
|
<div part="base" class="${classMap({
|
|
78
157
|
'cols': true,
|
|
@@ -87,6 +87,14 @@
|
|
|
87
87
|
border-bottom: 1px solid rgb(var(--zn-border-color)) !important;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// Children marked with [stack-split] act as a single column while the cols are
|
|
91
|
+
// side by side, and dissolve into individual columns once stacked (see below).
|
|
92
|
+
::slotted([stack-split]) {
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
gap: var(--zn-gap);
|
|
96
|
+
}
|
|
97
|
+
|
|
90
98
|
&--pad {
|
|
91
99
|
padding: var(--zn-spacing-2x-small);
|
|
92
100
|
}
|
|
@@ -99,3 +107,40 @@
|
|
|
99
107
|
padding-block: var(--zn-spacing-2x-small);
|
|
100
108
|
}
|
|
101
109
|
}
|
|
110
|
+
|
|
111
|
+
//
|
|
112
|
+
// Responsive stacking
|
|
113
|
+
//
|
|
114
|
+
// `stack-at` names the container width the cols collapse to a single column below.
|
|
115
|
+
// While stacked, `--zn-stacked` flips to 1 so the per-child `order: calc(var(--zn-stacked) * n)`
|
|
116
|
+
// written by the component only takes effect in the stacked state, and `--zn-stack-basis`
|
|
117
|
+
// gives promoted [stack-split] children something to size against.
|
|
118
|
+
//
|
|
119
|
+
// `:host()` is only in the selector to out-specify the `.zn-col-#{$i}` min-width rules above.
|
|
120
|
+
//
|
|
121
|
+
$stack-sizes: (sm, smp, ph, md, lg, hd, '3k', '4k');
|
|
122
|
+
|
|
123
|
+
@each $size in $stack-sizes {
|
|
124
|
+
:host([stack-at="#{$size}"]) {
|
|
125
|
+
@include wc.container-query(null, $size) {
|
|
126
|
+
.cols ::slotted(*) {
|
|
127
|
+
--zn-stacked: 1;
|
|
128
|
+
--zn-stack-basis: 100%;
|
|
129
|
+
|
|
130
|
+
flex-basis: 100%;
|
|
131
|
+
min-width: 100%;
|
|
132
|
+
max-width: 100%;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Hand this column's children over to the cols so each can be ordered individually
|
|
136
|
+
.cols ::slotted([stack-split]) {
|
|
137
|
+
display: contents;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Vertical dividers make no sense once the columns are on top of each other
|
|
141
|
+
.cols--divide ::slotted(*:not(:last-child)):after {
|
|
142
|
+
display: none;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -1,10 +1,138 @@
|
|
|
1
1
|
import '../../../dist/zn.min.js';
|
|
2
|
-
import { expect, fixture
|
|
2
|
+
import {aTimeout, expect, fixture} from '@open-wc/testing';
|
|
3
|
+
import type {LitElement} from 'lit';
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
/** Reads the flex order the browser actually resolved for a light DOM child */
|
|
6
|
+
const orderOf = (el: Element | null) => getComputedStyle(el!).order;
|
|
7
|
+
|
|
8
|
+
const cols = async (markup: string) => {
|
|
9
|
+
const el = await fixture<LitElement>(markup);
|
|
10
|
+
await el.updateComplete;
|
|
11
|
+
return el;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe('<zn-cols>', () => {
|
|
5
15
|
it('should render a component', async () => {
|
|
6
|
-
const el = await fixture(
|
|
16
|
+
const el = await fixture('<zn-cols></zn-cols>');
|
|
7
17
|
|
|
8
18
|
expect(el).to.exist;
|
|
9
19
|
});
|
|
20
|
+
|
|
21
|
+
it('should assign column classes from the layout pattern', async () => {
|
|
22
|
+
const el = await cols(`
|
|
23
|
+
<zn-cols layout="2,1">
|
|
24
|
+
<div id="main"></div>
|
|
25
|
+
<div id="side"></div>
|
|
26
|
+
<div id="wrapped"></div>
|
|
27
|
+
</zn-cols>`);
|
|
28
|
+
|
|
29
|
+
expect(el.querySelector('#main')).to.have.class('zn-col-2');
|
|
30
|
+
expect(el.querySelector('#side')).to.have.class('zn-col-1');
|
|
31
|
+
expect(el.querySelector('#wrapped')).to.have.class('zn-col-2');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('stacking', () => {
|
|
35
|
+
it('should not stack unless asked to', async () => {
|
|
36
|
+
const el = await cols(`
|
|
37
|
+
<zn-cols layout="2,1">
|
|
38
|
+
<div></div>
|
|
39
|
+
<div></div>
|
|
40
|
+
</zn-cols>`);
|
|
41
|
+
|
|
42
|
+
expect(el.getAttribute('stack-at')).to.equal('');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should default stack-at when a child declares a stack order', async () => {
|
|
46
|
+
const el = await cols(`
|
|
47
|
+
<zn-cols layout="2,1">
|
|
48
|
+
<div></div>
|
|
49
|
+
<div stack-order="first"></div>
|
|
50
|
+
</zn-cols>`);
|
|
51
|
+
await el.updateComplete;
|
|
52
|
+
|
|
53
|
+
expect(el.getAttribute('stack-at')).to.equal('lg');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should keep an explicit stack-at', async () => {
|
|
57
|
+
const el = await cols(`
|
|
58
|
+
<zn-cols layout="2,1" stack-at="md">
|
|
59
|
+
<div></div>
|
|
60
|
+
<div stack-order="first"></div>
|
|
61
|
+
</zn-cols>`);
|
|
62
|
+
await el.updateComplete;
|
|
63
|
+
|
|
64
|
+
expect(el.getAttribute('stack-at')).to.equal('md');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should only apply the stack order once stacked', async () => {
|
|
68
|
+
const el = await cols(`
|
|
69
|
+
<zn-cols layout="2,1" stack-at="md" style="width: 900px">
|
|
70
|
+
<div id="main"></div>
|
|
71
|
+
<div id="side" stack-order="first"></div>
|
|
72
|
+
</zn-cols>`);
|
|
73
|
+
|
|
74
|
+
expect(orderOf(el.querySelector('#side'))).to.equal('0');
|
|
75
|
+
|
|
76
|
+
el.style.width = '500px';
|
|
77
|
+
await aTimeout(50);
|
|
78
|
+
|
|
79
|
+
expect(orderOf(el.querySelector('#side'))).to.equal('-1');
|
|
80
|
+
expect(orderOf(el.querySelector('#main'))).to.equal('0');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should accept keywords and integers as a stack order', async () => {
|
|
84
|
+
const el = await cols(`
|
|
85
|
+
<zn-cols layout="2,1" stack-at="md" style="width: 500px">
|
|
86
|
+
<div id="a" stack-order="high"></div>
|
|
87
|
+
<div id="b" stack-order="low"></div>
|
|
88
|
+
<div id="c" stack-order="3"></div>
|
|
89
|
+
<div id="d" stack-order="nonsense"></div>
|
|
90
|
+
</zn-cols>`);
|
|
91
|
+
|
|
92
|
+
expect(orderOf(el.querySelector('#a'))).to.equal('-1');
|
|
93
|
+
expect(orderOf(el.querySelector('#b'))).to.equal('1');
|
|
94
|
+
expect(orderOf(el.querySelector('#c'))).to.equal('3');
|
|
95
|
+
expect(orderOf(el.querySelector('#d'))).to.equal('0');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should split a column into individually ordered children once stacked', async () => {
|
|
99
|
+
const el = await cols(`
|
|
100
|
+
<zn-cols layout="2,1" stack-at="md" style="width: 900px">
|
|
101
|
+
<div id="main" style="height: 100px"></div>
|
|
102
|
+
<div id="side" stack-split>
|
|
103
|
+
<div id="usage" stack-order="high" style="height: 20px"></div>
|
|
104
|
+
<div id="tutorials" stack-order="low" style="height: 20px"></div>
|
|
105
|
+
</div>
|
|
106
|
+
</zn-cols>`);
|
|
107
|
+
|
|
108
|
+
// Side by side, the split column is a single column holding both of its children
|
|
109
|
+
expect(getComputedStyle(el.querySelector('#side')!).display).to.equal('flex');
|
|
110
|
+
expect(orderOf(el.querySelector('#usage'))).to.equal('0');
|
|
111
|
+
|
|
112
|
+
el.style.width = '500px';
|
|
113
|
+
await aTimeout(50);
|
|
114
|
+
|
|
115
|
+
// Stacked, the column dissolves so each of its children becomes a column of its own
|
|
116
|
+
expect(getComputedStyle(el.querySelector('#side')!).display).to.equal('contents');
|
|
117
|
+
expect(orderOf(el.querySelector('#usage'))).to.equal('-1');
|
|
118
|
+
expect(orderOf(el.querySelector('#tutorials'))).to.equal('1');
|
|
119
|
+
|
|
120
|
+
const top = (selector: string) => el.querySelector(selector)!.getBoundingClientRect().top;
|
|
121
|
+
expect(top('#usage')).to.be.lessThan(top('#main'));
|
|
122
|
+
expect(top('#tutorials')).to.be.greaterThan(top('#main'));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should order children added after the initial render', async () => {
|
|
126
|
+
const el = await cols(`
|
|
127
|
+
<zn-cols layout="2,1" stack-at="md" style="width: 500px">
|
|
128
|
+
<div id="main"></div>
|
|
129
|
+
<div id="side" stack-split></div>
|
|
130
|
+
</zn-cols>`);
|
|
131
|
+
|
|
132
|
+
el.querySelector('#side')!.innerHTML = '<div id="usage" stack-order="high"></div>';
|
|
133
|
+
await aTimeout(50);
|
|
134
|
+
|
|
135
|
+
expect(orderOf(el.querySelector('#usage'))).to.equal('-1');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
10
138
|
});
|
|
@@ -33,11 +33,17 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
|
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
private readonly formControlController: FormControlController = new FormControlController(this);
|
|
36
|
-
private readonly hasSlotController = new HasSlotController(this, 'label', 'expand');
|
|
36
|
+
private readonly hasSlotController = new HasSlotController(this, 'label', 'expand', 'help-text');
|
|
37
37
|
|
|
38
38
|
@property() name = '';
|
|
39
39
|
@property() value = '{"en":""}';
|
|
40
40
|
@property() label: string = '';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Text shown below the field, describing how to fill it in. Applies to every language. If you need HTML, use the
|
|
44
|
+
* `help-text` slot instead.
|
|
45
|
+
*/
|
|
46
|
+
@property({attribute: 'help-text'}) helpText: string = '';
|
|
41
47
|
@property({type: Boolean, reflect: true}) disabled = false;
|
|
42
48
|
@property({type: Boolean, reflect: true}) required = false;
|
|
43
49
|
@property({type: Boolean, reflect: true}) flush = false;
|
|
@@ -356,6 +362,8 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
|
|
|
356
362
|
const hasLabelSlot = this.hasSlotController.test('label');
|
|
357
363
|
const hasLabel = this.label ? true : hasLabelSlot;
|
|
358
364
|
const hasExpandSlot = this.hasSlotController.test('expand');
|
|
365
|
+
const hasHelpTextSlot = this.hasSlotController.test('help-text');
|
|
366
|
+
const hasHelpText = this.helpText ? true : hasHelpTextSlot;
|
|
359
367
|
const hasMultipleLanguages = Object.keys(this.languages).length > 1;
|
|
360
368
|
const showActions = !this.grouped && (hasMultipleLanguages || hasExpandSlot);
|
|
361
369
|
|
|
@@ -368,6 +376,7 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
|
|
|
368
376
|
'form-control': true,
|
|
369
377
|
'form-control--medium': true,
|
|
370
378
|
'form-control--has-label': hasLabel,
|
|
379
|
+
'form-control--has-help-text': hasHelpText,
|
|
371
380
|
})}"
|
|
372
381
|
@keydown="${this.handleKeyDown}">
|
|
373
382
|
<div class="translations__header">
|
|
@@ -443,6 +452,12 @@ export default class ZnTranslations extends ZincElement implements ZincFormContr
|
|
|
443
452
|
></zn-inline-edit>
|
|
444
453
|
`)}
|
|
445
454
|
</div>
|
|
455
|
+
|
|
456
|
+
${hasHelpText ? html`
|
|
457
|
+
<div part="form-control-help-text" class="form-control__help-text" aria-hidden="false">
|
|
458
|
+
<slot name="help-text">${this.helpText}</slot>
|
|
459
|
+
</div>
|
|
460
|
+
` : nothing}
|
|
446
461
|
</div>
|
|
447
462
|
`;
|
|
448
463
|
}
|
|
@@ -50,6 +50,48 @@ describe('<zn-translations>', () => {
|
|
|
50
50
|
await expect(JSON.parse(el.value)).to.deep.equal({'en': 'Hello World'});
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
describe('help text', () => {
|
|
54
|
+
function helpTextOf(el: ZnTranslations) {
|
|
55
|
+
return el.shadowRoot!.querySelector<HTMLElement>('[part="form-control-help-text"]');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
it('is not rendered when unset', async () => {
|
|
59
|
+
const el = await fixture<ZnTranslations>(html`<zn-translations></zn-translations>`);
|
|
60
|
+
await el.updateComplete;
|
|
61
|
+
|
|
62
|
+
expect(helpTextOf(el)).to.not.exist;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('renders the help-text attribute below the field', async () => {
|
|
66
|
+
const el = await fixture<ZnTranslations>(html`
|
|
67
|
+
<zn-translations help-text="Type / to insert a replacement"></zn-translations>`);
|
|
68
|
+
await el.updateComplete;
|
|
69
|
+
|
|
70
|
+
expect(helpTextOf(el)?.textContent?.trim()).to.equal('Type / to insert a replacement');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('renders the help-text slot', async () => {
|
|
74
|
+
const el = await fixture<ZnTranslations>(html`
|
|
75
|
+
<zn-translations>
|
|
76
|
+
<div slot="help-text">Type <strong>/</strong> to insert</div>
|
|
77
|
+
</zn-translations>`);
|
|
78
|
+
await el.updateComplete;
|
|
79
|
+
|
|
80
|
+
expect(helpTextOf(el), 'a slotted help text should still render the wrapper').to.exist;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('shows the same help text after switching language', async () => {
|
|
84
|
+
const el = await fixture<ZnTranslations>(html`
|
|
85
|
+
<zn-translations help-text="Type / to insert a replacement"></zn-translations>`);
|
|
86
|
+
el.languages = {'en': 'EN', 'fr': 'FR'};
|
|
87
|
+
el.values = {'en': '', 'fr': ''};
|
|
88
|
+
el.setActiveLanguage('fr');
|
|
89
|
+
await el.updateComplete;
|
|
90
|
+
|
|
91
|
+
expect(helpTextOf(el)?.textContent?.trim()).to.equal('Type / to insert a replacement');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
53
95
|
describe('slash menu', () => {
|
|
54
96
|
/** Puts the active language's field into edit mode, the way clicking it does. */
|
|
55
97
|
async function textareaOf(el: ZnTranslations) {
|