@kubex/zinc 1.1.69 → 1.1.71

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.
@@ -33,7 +33,7 @@ import styles from './input.scss';
33
33
  *
34
34
  * @slot label - The input's label. Alternatively, you can use the `label` attribute.
35
35
  * @slot label-tooltip - Used to add text that is displayed in a tooltip next to the label. Alternatively, you can use the `label-tooltip` attribute.
36
- * @slot context-note - Used to add contextual text that is displayed above the input, on the right. Alternatively, you can use the `context-note` attribute.
36
+ * @slot context-note - Used to add contextual text that is displayed above the input, on the right. Alternatively, you can use the `context-note` attribute. Range inputs display their live value here unless this is set.
37
37
  * @slot prefix - Used to prepend a presentational icon or similar element to the input.
38
38
  * @slot suffix - Used to append a presentational icon or similar element to the input.
39
39
  * @slot clear-icon - An icon to use in lieu of the default clear icon.
@@ -51,6 +51,12 @@ import styles from './input.scss';
51
51
  * @csspart clear-button - The clear button.
52
52
  * @csspart password-toggle-button - The password toggle button.
53
53
  * @csspart suffix - The container that wraps the suffix.
54
+ *
55
+ * @cssproperty --zn-range-track-height - The height of a range input's track.
56
+ * @cssproperty --zn-range-track-color - The color of a range input's track.
57
+ * @cssproperty --zn-range-thumb-size - The diameter of a range input's thumb.
58
+ * @cssproperty --zn-range-thumb-color - The color of a range input's thumb.
59
+ * @cssproperty --zn-range-thumb-ring-width - The width of the ring drawn around a range input's thumb.
54
60
  */
55
61
  export default class ZnInput extends ZincElement implements ZincFormControl {
56
62
  static styles = unsafeCSS(styles);
@@ -80,7 +86,7 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
80
86
  * to `text`
81
87
  */
82
88
  @property({reflect: true}) type: 'color' | 'currency' | 'date' | 'datetime-local' | 'email' | 'number' | 'password' |
83
- 'search' | 'tel' | 'text' | 'time' | 'url' = 'text';
89
+ 'range' | 'search' | 'tel' | 'text' | 'time' | 'url' = 'text';
84
90
 
85
91
  /** The name of the input, submitted as a name/value pair with form data. */
86
92
  @property() name: string = "";
@@ -146,6 +152,12 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
146
152
  /** The color format to display for color inputs. Only applies when type is 'color'. **/
147
153
  @property({attribute: 'color-format'}) colorFormat: 'hex' | 'rgb' | 'hsl' | 'oklch' = 'hex';
148
154
 
155
+ /**
156
+ * Appended to the value that range inputs display above the track, on the right, e.g. `rem`. Only applies when type
157
+ * is 'range' and no `context-note` is set.
158
+ */
159
+ @property({attribute: 'value-suffix'}) valueSuffix: string = '';
160
+
149
161
  /**
150
162
  * By default, form-controls are associated with the nearest containing `<form>` element. This attribute allows you
151
163
  * to place the form control outside a form and associate it with the form that has this `id`. The form must be
@@ -615,6 +627,13 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
615
627
  }
616
628
  }
617
629
 
630
+ protected firstUpdated() {
631
+ // A range input with no value resolves to the midpoint of min/max natively, so adopt it as our value
632
+ if (this.type === 'range' && this.value === '') {
633
+ this.value = this.input.value;
634
+ }
635
+ }
636
+
618
637
  @watch('disabled', {waitUntilFirstUpdate: true})
619
638
  handleDisabledChange() {
620
639
  // Disabled form controls are always valid
@@ -763,9 +782,10 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
763
782
  const hasLabelTooltipSlot = this.hasSlotController.test('label-tooltip');
764
783
  const hasContextNoteSlot = this.hasSlotController.test('context-note');
765
784
  const hasHelpTextSlot = this.hasSlotController.test('help-text');
785
+ const isRange = this.type === 'range';
766
786
  const hasLabel = this.label ? true : hasLabelSlot;
767
787
  const hasLabelTooltip = this.labelTooltip ? true : hasLabelTooltipSlot;
768
- const hasContextNote = this.contextNote ? true : hasContextNoteSlot;
788
+ const hasContextNote = this.contextNote || isRange ? true : hasContextNoteSlot;
769
789
  const hasHelpText = this.helpText ? true : hasHelpTextSlot;
770
790
  const hasClearIcon = this.clearable && !this.disabled && !this.readonly;
771
791
  const hasOptionalIcon = this.optionalIcon;
@@ -803,7 +823,8 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
803
823
 
804
824
  ${hasContextNote
805
825
  ? html`
806
- <span class="form-control__label-context-note"><slot name="context-note">${this.contextNote}</slot></span>`
826
+ <span class="form-control__label-context-note"><slot name="context-note">${this.contextNote ||
827
+ (isRange ? `${this.value ?? ''}${this.valueSuffix}` : '')}</slot></span>`
807
828
  : ''}
808
829
 
809
830
  <div part="form-control-input"
@@ -822,27 +843,12 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
822
843
  'input--filled': this.filled,
823
844
  'input--focused': this.hasFocus,
824
845
  'input--empty': !this.value,
846
+ 'input--range': isRange,
825
847
  'input--no-spin-buttons': this.noSpinButtons && this.type !== 'currency',
826
848
  })}>
827
849
 
828
850
  <span part="prefix" class="input__prefix">
829
- ${this.type === 'color'
830
- ? html`
831
- <div
832
- class="input__color-swatch"
833
- @click=${this.handleColorSwatchClick}
834
- style="--color-value: ${this.convertToHex(this.value) || '#000000'}">
835
- <div class="input__color-swatch-inner"></div>
836
- </div>
837
- <input
838
- class="input__color-picker"
839
- type="color"
840
- .value=${this.convertToHex(this.value) || '#000000'}
841
- @change=${this.handleColorPickerChange}
842
- @input=${this.handleColorPickerChange}
843
- tabindex="-1"
844
- />`
845
- : this.type === 'currency'
851
+ ${this.type === 'currency'
846
852
  ? html`
847
853
  <zn-icon class="input__prefix-default" src="payments"></zn-icon>`
848
854
  : this.type === 'email' && hasOptionalIcon
@@ -933,6 +939,23 @@ export default class ZnInput extends ZincElement implements ZincFormControl {
933
939
 
934
940
  <span part="suffix" class="input__suffix">
935
941
  <slot name="suffix"></slot>
942
+ ${this.type === 'color'
943
+ ? html`
944
+ <div
945
+ class="input__color-swatch"
946
+ @click=${this.handleColorSwatchClick}
947
+ style="--color-value: ${this.convertToHex(this.value) || '#000000'}">
948
+ <div class="input__color-swatch-inner"></div>
949
+ </div>
950
+ <input
951
+ class="input__color-picker"
952
+ type="color"
953
+ .value=${this.convertToHex(this.value) || '#000000'}
954
+ @change=${this.handleColorPickerChange}
955
+ @input=${this.handleColorPickerChange}
956
+ tabindex="-1"
957
+ />`
958
+ : ''}
936
959
  </span>
937
960
  </div>
938
961
  </div>
@@ -384,51 +384,163 @@ input[type='date']::-webkit-calendar-picker-indicator {
384
384
  background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" height="14" width="12.25" viewBox="0 0 448 512"><path fill="%236d7176" d="M128 0c13.3 0 24 10.7 24 24V64H296V24c0-13.3 10.7-24 24-24s24 10.7 24 24V64h40c35.3 0 64 28.7 64 64v16 48V448c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V192 144 128C0 92.7 28.7 64 64 64h40V24c0-13.3 10.7-24 24-24zM400 192H48V448c0 8.8 7.2 16 16 16H384c8.8 0 16-7.2 16-16V192zM112 256h96c8.8 0 16 7.2 16 16v96c0 8.8-7.2 16-16 16H112c-8.8 0-16-7.2-16-16V272c0-8.8 7.2-16 16-16z"/></svg>');
385
385
  }
386
386
 
387
- /* Color input styling */
387
+ /* Color input styling — the preview fills the trailing edge of the field */
388
388
  .input__color-swatch {
389
389
  display: inline-flex;
390
- align-items: center;
391
- justify-content: center;
392
- width: 1.75rem;
393
- height: 1.75rem;
394
- border-radius: var(--zn-border-radius);
395
- border: solid 1px rgba(0, 0, 0, 0.15);
390
+ align-self: stretch;
391
+ border: none;
392
+ border-inline-start: solid var(--zn-input-border-width) var(--zn-input-border-color);
393
+ border-radius: 0;
396
394
  background-color: var(--zn-color-neutral-0);
397
395
  cursor: pointer;
398
- transition: var(--zn-transition-fast) border-color, var(--zn-transition-fast) box-shadow;
399
396
  overflow: hidden;
400
397
  position: relative;
401
398
  flex-shrink: 0;
402
- margin-inline-start: var(--zn-input-spacing-small);
399
+ margin: 0;
403
400
 
404
- &:hover {
405
- border-color: rgba(0, 0, 0, 0.25);
406
- box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.05);
401
+ &:hover .input__color-swatch-inner {
402
+ filter: brightness(0.95);
407
403
  }
408
404
  }
409
405
 
410
- /* Size-specific spacing adjustments for color swatch */
406
+ .input__color-swatch-inner {
407
+ width: 100%;
408
+ height: 100%;
409
+ background-color: var(--color-value, #000000);
410
+ transition: var(--zn-transition-fast) filter;
411
+ }
412
+
413
+ /* Size adjustments for color inputs — .input clips the trailing corners */
411
414
  .input--x-small .input__color-swatch {
412
- margin-inline-start: var(--zn-input-spacing-x-small);
415
+ width: calc(var(--zn-input-height-x-small) * 1.2);
413
416
  }
414
417
 
415
418
  .input--small .input__color-swatch {
416
- margin-inline-start: var(--zn-spacing-x2-small);
419
+ width: calc(var(--zn-input-height-small) * 1.2);
417
420
  }
418
421
 
419
422
  .input--medium .input__color-swatch {
420
- margin-inline-start: var(--zn-spacing-x-small);
423
+ width: calc(var(--zn-input-height-medium) * 1.2);
421
424
  }
422
425
 
423
426
  .input--large .input__color-swatch {
424
- margin-inline-start: var(--zn-spacing-small);
427
+ width: calc(var(--zn-input-height-large) * 1.2);
425
428
  }
426
429
 
427
- .input__color-swatch-inner {
428
- width: 100%;
429
- height: 100%;
430
- border-radius: calc(var(--zn-border-radius) - 2px);
431
- background-color: var(--color-value, #000000);
430
+ /* Range inputs render as a bare track, with the value shown in the context note above */
431
+ /* The thumb already leaves clear space above the track, so drop the label/field gap */
432
+ :host([type='range']) .form-control--has-label .form-control__label,
433
+ :host([type='range']) .form-control__label-context-note {
434
+ margin-bottom: 0;
435
+ }
436
+
437
+ :host([type='range']) .form-control-input {
438
+ margin-top: 0;
439
+ }
440
+
441
+ .input--standard.input--range {
442
+ --zn-range-track-height: 6px;
443
+ --zn-range-track-color: var(--zn-color-neutral-200);
444
+ --zn-range-thumb-size: 18px;
445
+ --zn-range-thumb-color: var(--zn-color-primary-600);
446
+ --zn-range-thumb-ring-width: 4px;
447
+
448
+ align-items: center;
449
+ height: var(--zn-range-thumb-size);
450
+ padding: 0;
451
+ overflow: visible;
452
+ cursor: pointer;
453
+ }
454
+
455
+ /* Matches the specificity of the hover/focus/disabled chrome rules above so the track stays bare */
456
+ .input--standard.input--range,
457
+ .input--standard.input--range:hover,
458
+ .input--standard.input--range.input--focused,
459
+ .input--standard.input--range.input--disabled {
460
+ border: none;
461
+ background: none;
462
+ box-shadow: none;
463
+ }
464
+
465
+ .input--standard.input--range.input--x-small {
466
+ --zn-range-track-height: 4px;
467
+ --zn-range-thumb-size: 14px;
468
+ --zn-range-thumb-ring-width: 3px;
469
+ }
470
+
471
+ .input--standard.input--range.input--small {
472
+ --zn-range-track-height: 5px;
473
+ --zn-range-thumb-size: 16px;
474
+ --zn-range-thumb-ring-width: 3px;
475
+ }
476
+
477
+ .input--standard.input--range.input--large {
478
+ --zn-range-track-height: 7px;
479
+ --zn-range-thumb-size: 20px;
480
+ }
481
+
482
+ .input--range .input__control {
483
+ -webkit-appearance: none;
484
+ appearance: none;
485
+ height: var(--zn-range-thumb-size);
486
+ padding: 0;
487
+ margin: 0;
488
+ background: none;
489
+ cursor: inherit;
490
+ }
491
+
492
+ .input--range .input__control::-webkit-slider-runnable-track {
493
+ height: var(--zn-range-track-height);
494
+ border-radius: calc(var(--zn-range-track-height) / 2);
495
+ background-color: var(--zn-range-track-color);
496
+ }
497
+
498
+ .input--range .input__control::-moz-range-track {
499
+ height: var(--zn-range-track-height);
500
+ border-radius: calc(var(--zn-range-track-height) / 2);
501
+ background-color: var(--zn-range-track-color);
502
+ }
503
+
504
+ .input--range .input__control::-webkit-slider-thumb {
505
+ -webkit-appearance: none;
506
+ appearance: none;
507
+ width: var(--zn-range-thumb-size);
508
+ height: var(--zn-range-thumb-size);
509
+ margin-top: calc((var(--zn-range-track-height) - var(--zn-range-thumb-size)) / 2);
510
+ border: var(--zn-range-thumb-ring-width) solid var(--zn-color-neutral-0);
511
+ border-radius: 50%;
512
+ background-color: var(--zn-range-thumb-color);
513
+ box-shadow: 0 0 0 1px var(--zn-input-border-color);
514
+ transition: var(--zn-transition-fast) box-shadow, var(--zn-transition-fast) background-color;
515
+ }
516
+
517
+ .input--range .input__control::-moz-range-thumb {
518
+ width: var(--zn-range-thumb-size);
519
+ height: var(--zn-range-thumb-size);
520
+ box-sizing: border-box;
521
+ border: var(--zn-range-thumb-ring-width) solid var(--zn-color-neutral-0);
522
+ border-radius: 50%;
523
+ background-color: var(--zn-range-thumb-color);
524
+ box-shadow: 0 0 0 1px var(--zn-input-border-color);
525
+ transition: var(--zn-transition-fast) box-shadow, var(--zn-transition-fast) background-color;
526
+ }
527
+
528
+ .input--range:hover:not(.input--disabled) .input__control::-webkit-slider-thumb {
529
+ background-color: var(--zn-color-primary-500);
530
+ box-shadow: 0 0 0 1px var(--zn-input-border-color-hover);
531
+ }
532
+
533
+ .input--range:hover:not(.input--disabled) .input__control::-moz-range-thumb {
534
+ background-color: var(--zn-color-primary-500);
535
+ box-shadow: 0 0 0 1px var(--zn-input-border-color-hover);
536
+ }
537
+
538
+ .input--range .input__control:focus-visible::-webkit-slider-thumb {
539
+ box-shadow: 0 0 0 var(--zn-focus-ring-width) var(--zn-input-focus-ring-color);
540
+ }
541
+
542
+ .input--range .input__control:focus-visible::-moz-range-thumb {
543
+ box-shadow: 0 0 0 var(--zn-focus-ring-width) var(--zn-input-focus-ring-color);
432
544
  }
433
545
 
434
546
  /* Hide the native color input but keep it functional */
@@ -444,23 +556,3 @@ input[type='date']::-webkit-calendar-picker-indicator {
444
556
  visibility: hidden;
445
557
  }
446
558
 
447
- /* Size adjustments for color inputs */
448
- .input--x-small .input__color-swatch {
449
- width: 1.25rem;
450
- height: 1.25rem;
451
- }
452
-
453
- .input--small .input__color-swatch {
454
- width: 1.5rem;
455
- height: 1.5rem;
456
- }
457
-
458
- .input--medium .input__color-swatch {
459
- width: 1.75rem;
460
- height: 1.75rem;
461
- }
462
-
463
- .input--large .input__color-swatch {
464
- width: 2rem;
465
- height: 2rem;
466
- }
@@ -172,4 +172,49 @@ describe('<zn-input>', () => {
172
172
  expect(secondInput.value).to.equal('');
173
173
  });
174
174
  });
175
+
176
+ describe('type="range"', () => {
177
+ const contextNote = (el: ZnInput) =>
178
+ el.shadowRoot!.querySelector('.form-control__label-context-note')!.textContent!.trim();
179
+
180
+ it('adopts the native midpoint when no value is set', async () => {
181
+ const el = await fixture<ZnInput>(html`<zn-input type="range" min="0" max="10"></zn-input>`);
182
+ await el.updateComplete;
183
+
184
+ expect(el.value).to.equal('5');
185
+ });
186
+
187
+ it('displays the value with its suffix in the context note', async () => {
188
+ const el = await fixture<ZnInput>(
189
+ html`<zn-input type="range" label="Radius" min="0" max="2" step="0.5" value="0.5" value-suffix="rem"></zn-input>`
190
+ );
191
+ await el.updateComplete;
192
+
193
+ expect(contextNote(el)).to.equal('0.5rem');
194
+ });
195
+
196
+ it('updates the context note as the value changes', async () => {
197
+ const el = await fixture<ZnInput>(
198
+ html`<zn-input type="range" label="Radius" min="0" max="10" value="4" value-suffix="px"></zn-input>`
199
+ );
200
+ await el.updateComplete;
201
+
202
+ const input = el.shadowRoot!.querySelector('.input__control') as HTMLInputElement;
203
+ input.value = '7';
204
+ input.dispatchEvent(new Event('input'));
205
+ await el.updateComplete;
206
+
207
+ expect(el.value).to.equal('7');
208
+ expect(contextNote(el)).to.equal('7px');
209
+ });
210
+
211
+ it('prefers an explicit context-note over the value', async () => {
212
+ const el = await fixture<ZnInput>(
213
+ html`<zn-input type="range" label="Radius" context-note="Custom" value="4"></zn-input>`
214
+ );
215
+ await el.updateComplete;
216
+
217
+ expect(contextNote(el)).to.equal('Custom');
218
+ });
219
+ });
175
220
  });