@kubex/zinc 1.1.58 → 1.1.60
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 +272 -197
- package/dist/vscode.html-custom-data.json +41 -27
- package/dist/web-types.json +91 -53
- package/dist/zn.d.ts +12 -0
- package/dist/zn.min.js +126 -111
- package/docs/pages/components/input.md +30 -0
- package/docs/pages/components/toggle.md +62 -0
- package/package.json +1 -1
- package/src/components/input/input.component.ts +9 -2
- package/src/components/input/input.test.ts +55 -0
- package/src/components/toggle/toggle.component.ts +39 -3
- package/src/components/toggle/toggle.scss +66 -3
- package/src/components/toggle/toggle.test.ts +11 -0
- package/src/internal/form-navigation.ts +5 -0
|
@@ -416,6 +416,36 @@ Form navigation works with all Zinc form controls including selects, radios, che
|
|
|
416
416
|
**Note:** While you can navigate TO textareas and search inputs using Enter, pressing Enter while inside these controls uses the Enter key for their own functionality (new lines and search triggers, respectively). Use Tab to navigate away from these controls.
|
|
417
417
|
:::
|
|
418
418
|
|
|
419
|
+
### Trigger Submit
|
|
420
|
+
|
|
421
|
+
Add the `trigger-submit` attribute to make pressing Enter in the input always submit the surrounding form. Inputs already submit on Enter by default, so this is only needed on forms using `data-enter-navigation`, where Enter would otherwise move focus to the next field.
|
|
422
|
+
|
|
423
|
+
```html:preview
|
|
424
|
+
<form class="trigger-submit-form" data-enter-navigation>
|
|
425
|
+
<zn-input name="firstName" label="First Name" required></zn-input>
|
|
426
|
+
<br />
|
|
427
|
+
<zn-input name="search" label="Quick Search (Enter submits)" trigger-submit></zn-input>
|
|
428
|
+
<br />
|
|
429
|
+
<zn-button type="submit" variant="primary">Submit</zn-button>
|
|
430
|
+
</form>
|
|
431
|
+
|
|
432
|
+
<script type="module">
|
|
433
|
+
const form = document.querySelector('.trigger-submit-form');
|
|
434
|
+
|
|
435
|
+
await Promise.all([
|
|
436
|
+
customElements.whenDefined('zn-button'),
|
|
437
|
+
customElements.whenDefined('zn-input')
|
|
438
|
+
]).then(() => {
|
|
439
|
+
form.addEventListener('submit', (e) => {
|
|
440
|
+
e.preventDefault();
|
|
441
|
+
const formData = new FormData(form);
|
|
442
|
+
const data = Object.fromEntries(formData);
|
|
443
|
+
alert('Form submitted!\n\n' + JSON.stringify(data, null, 2));
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
</script>
|
|
447
|
+
```
|
|
448
|
+
|
|
419
449
|
### Customizing Label Position
|
|
420
450
|
|
|
421
451
|
Use [CSS parts](#css-parts) to customize the way form controls are drawn. This example uses CSS grid to position the
|
|
@@ -62,6 +62,68 @@ Use the `label` attribute to add a form control label above the toggle.
|
|
|
62
62
|
<zn-toggle label="User Preferences">Enable email notifications</zn-toggle>
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
### Description
|
|
66
|
+
|
|
67
|
+
Use the `description` attribute to add a description under the label. If you need to display HTML, use the `description` slot instead.
|
|
68
|
+
|
|
69
|
+
```html:preview
|
|
70
|
+
<zn-toggle label="Email notifications" description="Receive an email whenever someone mentions you."></zn-toggle>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The description stays under the label at every `label-position`, and can be combined with `help-text`, which always appears below the toggle.
|
|
74
|
+
|
|
75
|
+
```html:preview
|
|
76
|
+
<zn-toggle
|
|
77
|
+
label="Top label (default)"
|
|
78
|
+
description="Receive an email whenever someone mentions you."
|
|
79
|
+
help-text="You can change this at any time"></zn-toggle>
|
|
80
|
+
<br />
|
|
81
|
+
<zn-toggle
|
|
82
|
+
label="Left label"
|
|
83
|
+
label-position="left"
|
|
84
|
+
description="Receive an email whenever someone mentions you."
|
|
85
|
+
help-text="You can change this at any time"></zn-toggle>
|
|
86
|
+
<br />
|
|
87
|
+
<zn-toggle
|
|
88
|
+
label="Right label"
|
|
89
|
+
label-position="right"
|
|
90
|
+
description="Receive an email whenever someone mentions you."
|
|
91
|
+
help-text="You can change this at any time"></zn-toggle>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Label Position
|
|
95
|
+
|
|
96
|
+
Use the `label-position` attribute to control where the label sits relative to the toggle. Available positions are `top` (default), `left`, and `right`.
|
|
97
|
+
|
|
98
|
+
With `left`, the label sits at the start of the row and the toggle is pushed to the far right — useful for settings lists. With `right`, the layout is mirrored.
|
|
99
|
+
|
|
100
|
+
```html:preview
|
|
101
|
+
<zn-toggle label="Top label (default)"></zn-toggle>
|
|
102
|
+
<br />
|
|
103
|
+
<zn-toggle label="Left label" label-position="left"></zn-toggle>
|
|
104
|
+
<br />
|
|
105
|
+
<zn-toggle label="Right label" label-position="right"></zn-toggle>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Combine `label-position` with the `inline` attribute to keep the label and toggle adjacent instead of spread across the full width.
|
|
109
|
+
|
|
110
|
+
```html:preview
|
|
111
|
+
<zn-toggle label="Inline left" label-position="left" inline></zn-toggle>
|
|
112
|
+
<br />
|
|
113
|
+
<zn-toggle label="Inline right" label-position="right" inline></zn-toggle>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
A settings list using `label-position="left"`:
|
|
117
|
+
|
|
118
|
+
```html:preview
|
|
119
|
+
<div style="max-width: 320px;">
|
|
120
|
+
<zn-toggle label="Apple" label-position="left"></zn-toggle>
|
|
121
|
+
<zn-toggle label="Windows" label-position="left"></zn-toggle>
|
|
122
|
+
<zn-toggle label="Android" label-position="left"></zn-toggle>
|
|
123
|
+
<zn-toggle label="Blackberry" label-position="left" checked></zn-toggle>
|
|
124
|
+
</div>
|
|
125
|
+
```
|
|
126
|
+
|
|
65
127
|
### On/Off Text with Tooltip
|
|
66
128
|
|
|
67
129
|
Use the `on-text` and `off-text` attributes to display tooltips that show when the toggle is in the on or off state. This provides additional context for the toggle's current state.
|
package/package.json
CHANGED
|
@@ -212,6 +212,12 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
|
|
|
212
212
|
*/
|
|
213
213
|
@property() inputmode: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url';
|
|
214
214
|
|
|
215
|
+
/**
|
|
216
|
+
* When enabled, pressing enter will always submit the surrounding form, even when the form uses
|
|
217
|
+
* enter-navigation to move between fields.
|
|
218
|
+
*/
|
|
219
|
+
@property({type: Boolean, attribute: 'trigger-submit'}) triggerSubmit = false;
|
|
220
|
+
|
|
215
221
|
//
|
|
216
222
|
// NOTE: We use an in-memory input for these getters/setters instead of the one in the template because the properties
|
|
217
223
|
// can be set before the component is rendered.
|
|
@@ -561,9 +567,10 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
|
|
|
561
567
|
// When using a Input Method Editor (IME), pressing enter will cause the form to submit unexpectedly. One way
|
|
562
568
|
// to check for this is to look at event.isComposing, which will be true when the IME is open.
|
|
563
569
|
if (!event.defaultPrevented && !event.isComposing) {
|
|
564
|
-
// Check if form has navigation enabled - if so, let FormNavigationController handle it
|
|
570
|
+
// Check if form has navigation enabled - if so, let FormNavigationController handle it,
|
|
571
|
+
// unless trigger-submit is set which always submits on enter
|
|
565
572
|
const form = this.formControlController.getForm();
|
|
566
|
-
if (form?.hasAttribute('enter-navigation') || form?.hasAttribute('data-enter-navigation')) {
|
|
573
|
+
if (!this.triggerSubmit && (form?.hasAttribute('enter-navigation') || form?.hasAttribute('data-enter-navigation'))) {
|
|
567
574
|
return; // Let FormNavigationController handle it
|
|
568
575
|
}
|
|
569
576
|
|
|
@@ -42,6 +42,61 @@ describe('<zn-input>', () => {
|
|
|
42
42
|
});
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
describe('trigger-submit', () => {
|
|
46
|
+
const pressEnter = (el: ZnInput) => {
|
|
47
|
+
el.input.dispatchEvent(new KeyboardEvent('keydown', {
|
|
48
|
+
key: 'Enter',
|
|
49
|
+
bubbles: true,
|
|
50
|
+
composed: true,
|
|
51
|
+
cancelable: true
|
|
52
|
+
}));
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
it('submits an enter-navigation form on enter when trigger-submit is set', async () => {
|
|
56
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
57
|
+
<form data-enter-navigation>
|
|
58
|
+
<zn-input name="a" trigger-submit></zn-input>
|
|
59
|
+
<zn-input name="b"></zn-input>
|
|
60
|
+
</form>
|
|
61
|
+
`);
|
|
62
|
+
const el = form.querySelector('zn-input')!;
|
|
63
|
+
await el.updateComplete;
|
|
64
|
+
|
|
65
|
+
let submitted = false;
|
|
66
|
+
form.addEventListener('submit', e => {
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
submitted = true;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
el.focus();
|
|
72
|
+
pressEnter(el);
|
|
73
|
+
|
|
74
|
+
expect(submitted).to.be.true;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('does not submit an enter-navigation form on enter without trigger-submit', async () => {
|
|
78
|
+
const form = await fixture<HTMLFormElement>(html`
|
|
79
|
+
<form data-enter-navigation>
|
|
80
|
+
<zn-input name="a"></zn-input>
|
|
81
|
+
<zn-input name="b"></zn-input>
|
|
82
|
+
</form>
|
|
83
|
+
`);
|
|
84
|
+
const el = form.querySelector('zn-input')!;
|
|
85
|
+
await el.updateComplete;
|
|
86
|
+
|
|
87
|
+
let submitted = false;
|
|
88
|
+
form.addEventListener('submit', e => {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
submitted = true;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
el.focus();
|
|
94
|
+
pressEnter(el);
|
|
95
|
+
|
|
96
|
+
expect(submitted).to.be.false;
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
45
100
|
describe('persisted state', () => {
|
|
46
101
|
const formStoreKey = 'input-test-form-state';
|
|
47
102
|
const elementStoreKey = 'input-test-element-state';
|
|
@@ -2,6 +2,7 @@ import { classMap } from "lit/directives/class-map.js";
|
|
|
2
2
|
import { type CSSResultGroup, html, type PropertyValues, unsafeCSS } from 'lit';
|
|
3
3
|
import { defaultValue } from "../../internal/default-value";
|
|
4
4
|
import { FormControlController } from "../../internal/form";
|
|
5
|
+
import { HasSlotController } from "../../internal/slot";
|
|
5
6
|
import { ifDefined } from "lit/directives/if-defined.js";
|
|
6
7
|
import { live } from "lit/directives/live.js";
|
|
7
8
|
import { property, query, state } from 'lit/decorators.js';
|
|
@@ -20,15 +21,20 @@ import styles from './toggle.scss';
|
|
|
20
21
|
* @event zn-input - Emitted when the toggle receives input.
|
|
21
22
|
*
|
|
22
23
|
* @slot - The toggle's label.
|
|
24
|
+
* @slot description - A description of the toggle's label. Alternatively, you can use the `description` attribute.
|
|
25
|
+
* @slot help-text - Text that describes how to use the toggle. Alternatively, you can use the `help-text` attribute.
|
|
23
26
|
*
|
|
24
27
|
* @csspart base - The component's base wrapper containing the toggle switch.
|
|
25
28
|
* @csspart control - The toggle switch control (the circular button that slides).
|
|
29
|
+
* @csspart description - The container that wraps the toggle's description.
|
|
26
30
|
*
|
|
27
31
|
* @cssproperty --zn-toggle-margin - The margin around the toggle switch. Defaults to `8px 0`.
|
|
28
32
|
*/
|
|
29
33
|
export default class ZnToggle extends ZincElement implements ZincFormControl {
|
|
30
34
|
static styles: CSSResultGroup = unsafeCSS(styles);
|
|
31
35
|
|
|
36
|
+
private readonly hasSlotController = new HasSlotController(this, 'help-text', 'description');
|
|
37
|
+
|
|
32
38
|
private readonly formControlController = new FormControlController(this, {
|
|
33
39
|
value: (control: ZnToggle) => (control.checked ? control.value || 'on' : undefined),
|
|
34
40
|
defaultValue: (control: ZnToggle) => control.defaultChecked,
|
|
@@ -69,6 +75,11 @@ export default class ZnToggle extends ZincElement implements ZincFormControl {
|
|
|
69
75
|
|
|
70
76
|
@property() label: string = '';
|
|
71
77
|
|
|
78
|
+
/** The toggle's description, displayed under the label. If you need to display HTML, use the `description` slot instead. */
|
|
79
|
+
@property() description: string = '';
|
|
80
|
+
|
|
81
|
+
@property({ attribute: 'label-position', reflect: true }) labelPosition: 'top' | 'left' | 'right' = 'top';
|
|
82
|
+
|
|
72
83
|
@property({ type: Boolean }) inline: boolean = false;
|
|
73
84
|
|
|
74
85
|
get validity() {
|
|
@@ -167,6 +178,8 @@ export default class ZnToggle extends ZincElement implements ZincFormControl {
|
|
|
167
178
|
|
|
168
179
|
const tooltipContent = this.checked ? this.onText : this.offText;
|
|
169
180
|
const showTooltip = !!tooltipContent;
|
|
181
|
+
const hasHelpText = this.helpText ? true : this.hasSlotController.test('help-text');
|
|
182
|
+
const hasDescription = this.description ? true : this.hasSlotController.test('description');
|
|
170
183
|
|
|
171
184
|
const toggle = html`
|
|
172
185
|
<div class="switch__input-wrapper" part="base">
|
|
@@ -182,7 +195,7 @@ export default class ZnToggle extends ZincElement implements ZincFormControl {
|
|
|
182
195
|
.required=${this.required}
|
|
183
196
|
role="switch"
|
|
184
197
|
aria-checked=${this.checked ? 'true' : 'false'}
|
|
185
|
-
aria-describedby="help-text"
|
|
198
|
+
aria-describedby="description help-text"
|
|
186
199
|
@click=${this.handleClick}
|
|
187
200
|
@input=${this.handleInput}
|
|
188
201
|
@invalid=${this.handleInvalid}
|
|
@@ -195,22 +208,45 @@ export default class ZnToggle extends ZincElement implements ZincFormControl {
|
|
|
195
208
|
|
|
196
209
|
return html`
|
|
197
210
|
<div class="${classMap({
|
|
211
|
+
'form-control': true,
|
|
212
|
+
'form-control--small': this.size === 'small',
|
|
213
|
+
'form-control--medium': this.size === 'medium',
|
|
214
|
+
'form-control--large': this.size === 'large',
|
|
215
|
+
'form-control--has-help-text': hasHelpText,
|
|
198
216
|
'switch__wrapper': true,
|
|
217
|
+
'switch__wrapper--has-description': hasDescription,
|
|
199
218
|
'switch__wrapper--disabled': this.disabled,
|
|
200
219
|
'switch__wrapper--has-focus': this.hasFocus,
|
|
201
220
|
'switch__wrapper--small': this.size === 'small',
|
|
202
221
|
'switch__wrapper--medium': this.size === 'medium',
|
|
203
222
|
'switch__wrapper--large': this.size === 'large',
|
|
204
|
-
'switch__wrapper--inline': this.inline
|
|
223
|
+
'switch__wrapper--inline': this.inline,
|
|
224
|
+
'switch__wrapper--label-left': this.labelPosition === 'left',
|
|
225
|
+
'switch__wrapper--label-right': this.labelPosition === 'right'
|
|
205
226
|
})}">
|
|
206
227
|
<label>
|
|
207
|
-
${this.label ? html
|
|
228
|
+
${this.label || hasDescription ? html`
|
|
229
|
+
<div class="switch__label-wrapper">
|
|
230
|
+
${this.label ? html`<p class="switch-label">${this.label}</p>` : ''}
|
|
231
|
+
<div part="description"
|
|
232
|
+
id="description"
|
|
233
|
+
class="switch__description"
|
|
234
|
+
aria-hidden=${hasDescription ? 'false' : 'true'}>
|
|
235
|
+
<slot name="description">${this.description}</slot>
|
|
236
|
+
</div>
|
|
237
|
+
</div>` : ''}
|
|
208
238
|
${!showTooltip ? html`
|
|
209
239
|
${toggle}` : html`
|
|
210
240
|
<zn-tooltip content="${tooltipContent}">
|
|
211
241
|
${toggle}
|
|
212
242
|
</zn-tooltip>`}
|
|
213
243
|
</label>
|
|
244
|
+
<div part="form-control-help-text"
|
|
245
|
+
id="help-text"
|
|
246
|
+
class="form-control__help-text"
|
|
247
|
+
aria-hidden=${hasHelpText ? 'false' : 'true'}>
|
|
248
|
+
<slot name="help-text">${this.helpText}</slot>
|
|
249
|
+
</div>
|
|
214
250
|
</div>`;
|
|
215
251
|
}
|
|
216
252
|
}
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
@use "../../wc";
|
|
2
|
+
@use "../../form-control";
|
|
2
3
|
|
|
3
4
|
:host {
|
|
4
5
|
--zn-toggle-margin: 8px 0;
|
|
6
|
+
display: block;
|
|
5
7
|
}
|
|
6
8
|
|
|
7
9
|
.switch__wrapper {
|
|
8
|
-
display:
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
|
|
13
|
+
label {
|
|
14
|
+
flex: 1;
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
align-items: flex-start;
|
|
18
|
+
cursor: pointer;
|
|
19
|
+
}
|
|
9
20
|
}
|
|
10
21
|
|
|
11
22
|
.switch__input-wrapper {
|
|
@@ -47,7 +58,33 @@
|
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
.switch-label {
|
|
50
|
-
|
|
61
|
+
margin: 0;
|
|
62
|
+
color: var(--zn-input-label-color);
|
|
63
|
+
font-size: var(--zn-input-label-font-size-medium);
|
|
64
|
+
font-weight: var(--zn-input-label-font-weight);
|
|
65
|
+
line-height: var(--zn-input-label-line-height);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.switch__wrapper--small .switch-label {
|
|
69
|
+
font-size: var(--zn-input-label-font-size-small);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.switch__description {
|
|
73
|
+
display: none;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.switch__wrapper--has-description .switch__description {
|
|
77
|
+
display: block;
|
|
78
|
+
color: var(--zn-input-help-text-color);
|
|
79
|
+
font-size: var(--zn-input-font-size-small);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.switch__wrapper--small .switch__description {
|
|
83
|
+
font-size: var(--zn-font-size-x-small);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.switch__wrapper--large .switch-label {
|
|
87
|
+
font-size: var(--zn-input-label-font-size-large);
|
|
51
88
|
}
|
|
52
89
|
|
|
53
90
|
.switch__control {
|
|
@@ -91,10 +128,32 @@
|
|
|
91
128
|
}
|
|
92
129
|
}
|
|
93
130
|
|
|
131
|
+
// Label position
|
|
132
|
+
.switch__wrapper--label-left label,
|
|
133
|
+
.switch__wrapper--label-right label {
|
|
134
|
+
align-items: center;
|
|
135
|
+
justify-content: space-between;
|
|
136
|
+
gap: 8px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.switch__wrapper--label-left label {
|
|
140
|
+
flex-direction: row;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.switch__wrapper--label-right label {
|
|
144
|
+
flex-direction: row-reverse;
|
|
145
|
+
}
|
|
146
|
+
|
|
94
147
|
// Support inline label
|
|
148
|
+
:host([inline]) {
|
|
149
|
+
display: inline-block;
|
|
150
|
+
}
|
|
151
|
+
|
|
95
152
|
.switch__wrapper--inline label {
|
|
153
|
+
flex: none;
|
|
154
|
+
flex-direction: row;
|
|
96
155
|
align-items: center;
|
|
97
|
-
|
|
156
|
+
justify-content: flex-start;
|
|
98
157
|
gap: 8px;
|
|
99
158
|
|
|
100
159
|
.switch__input {
|
|
@@ -102,3 +161,7 @@
|
|
|
102
161
|
margin-inline-end: 1px;
|
|
103
162
|
}
|
|
104
163
|
}
|
|
164
|
+
|
|
165
|
+
.switch__wrapper--inline.switch__wrapper--label-right label {
|
|
166
|
+
flex-direction: row-reverse;
|
|
167
|
+
}
|
|
@@ -7,4 +7,15 @@ describe('<zn-toggle>', () => {
|
|
|
7
7
|
|
|
8
8
|
expect(el).to.exist;
|
|
9
9
|
});
|
|
10
|
+
|
|
11
|
+
it('should render the description under the label', async () => {
|
|
12
|
+
const el = await fixture<HTMLElement>(html`
|
|
13
|
+
<zn-toggle label="Email notifications" description="Receive an email when mentioned."></zn-toggle> `);
|
|
14
|
+
|
|
15
|
+
const label = el.shadowRoot!.querySelector('.switch-label')!;
|
|
16
|
+
const description = el.shadowRoot!.querySelector('[part="description"]')!;
|
|
17
|
+
|
|
18
|
+
expect(description.textContent).to.include('Receive an email when mentioned.');
|
|
19
|
+
expect(label.nextElementSibling).to.equal(description);
|
|
20
|
+
});
|
|
10
21
|
});
|
|
@@ -46,6 +46,11 @@ export class FormNavigationController {
|
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// Controls with trigger-submit handle submitting the form themselves
|
|
50
|
+
if (target.hasAttribute('trigger-submit')) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
49
54
|
// Check if we're in a group (radio-group or checkbox-group)
|
|
50
55
|
const group = this.findParentGroup(target);
|
|
51
56
|
if (group) {
|