@kubex/zinc 1.1.77 → 1.1.79

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.
@@ -74,6 +74,29 @@ Use the `disabled` attribute to disable the rating component.
74
74
  <zn-rating value="3" disabled label="Rating disabled with value"></zn-rating>
75
75
  ```
76
76
 
77
+ ### Help Text
78
+
79
+ Add descriptive help text to a rating with the `help-text` attribute. For help text that contains HTML, use the
80
+ `help-text` slot instead.
81
+
82
+ ```html:preview
83
+ <zn-rating label="Rate this product" help-text="Select a star to rate this product."></zn-rating>
84
+ <br />
85
+ <zn-rating value="3" precision="0.5" label="Overall rating">
86
+ <div slot="help-text">Ratings are <strong>public</strong> and can be changed at any time.</div>
87
+ </zn-rating>
88
+ ```
89
+
90
+ Help text is styled to match [Input](/components/input) and follows the `size` attribute.
91
+
92
+ ```html:preview
93
+ <zn-rating size="small" value="3" help-text="Small help text"></zn-rating>
94
+ <br />
95
+ <zn-rating size="medium" value="3" help-text="Medium help text"></zn-rating>
96
+ <br />
97
+ <zn-rating size="large" value="3" help-text="Large help text"></zn-rating>
98
+ ```
99
+
77
100
  ### Sizes
78
101
 
79
102
  Use the `size` attribute to change the rating size. Available sizes are `small`, `medium` (default), and `large`.
@@ -116,6 +139,40 @@ The rating component displays hover effects by default, scaling the hovered star
116
139
  **Usage:** Hover effects provide visual feedback to help users understand which rating they will select. The hover effect is automatically disabled for `readonly` and `disabled` ratings.
117
140
  :::
118
141
 
142
+ ### Preview
143
+
144
+ Use the `preview` attribute to show the value next to the symbols. While the pointer moves across the symbols the
145
+ preview follows it, so the value being selected is visible before it is committed. Once the pointer leaves, the preview
146
+ falls back to the selected value.
147
+
148
+ ```html:preview
149
+ <zn-rating preview label="Rate this product"></zn-rating>
150
+ <br />
151
+ <zn-rating preview value="2.5" precision="0.5" label="Half star precision"></zn-rating>
152
+ <br />
153
+ <zn-rating preview value="3.75" precision="0.25" max="10" size="large" label="Quarter star precision"></zn-rating>
154
+ ```
155
+
156
+ The preview is formatted to match `precision`, so a rating with `precision="0.5"` previews `4.5` rather than `4`.
157
+ It also works with `readonly` and `disabled` ratings, where it simply displays the current value.
158
+
159
+ ```html:preview
160
+ <zn-rating preview value="4.5" precision="0.5" readonly label="Average rating"></zn-rating>
161
+ <br />
162
+ <zn-rating preview value="3" disabled label="Rating disabled"></zn-rating>
163
+ ```
164
+
165
+ Style the preview with the `--preview-color` and `--preview-size` custom properties, or the `preview` part.
166
+
167
+ ```html:preview
168
+ <zn-rating
169
+ preview
170
+ value="4"
171
+ label="Custom preview styling"
172
+ style="--preview-color: #3B82F6; --preview-size: 1.25rem;"
173
+ ></zn-rating>
174
+ ```
175
+
119
176
  ### Form Integration
120
177
 
121
178
  Ratings work seamlessly with forms and will be submitted with form data using the `name` attribute.
@@ -271,7 +328,10 @@ Available CSS custom properties:
271
328
  - `--symbol-color` - Color of unfilled symbols
272
329
  - `--symbol-color-active` - Color of filled/active symbols
273
330
  - `--symbol-size` - Size of the symbols
274
- - `--symbol-spacing` - Spacing between symbols
331
+ - `--symbol-spacing` - Gap between symbols. The rating adds no padding of its own, so the first symbol sits flush with
332
+ the component's edge and lines up with other form controls.
333
+ - `--preview-color` - Color of the preview value (see [Preview](#preview))
334
+ - `--preview-size` - Font size of the preview value
275
335
 
276
336
  ### Complete Example
277
337
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubex/zinc",
3
- "version": "1.1.77",
3
+ "version": "1.1.79",
4
4
  "description": "A collection of web components for building web applications based off of @shoelace-style/Shoelace",
5
5
  "keywords": [
6
6
  "web components",
@@ -1,6 +1,7 @@
1
1
  import {classMap} from "lit/directives/class-map.js";
2
2
  import {type CSSResultGroup, html, unsafeCSS} from 'lit';
3
3
  import {FormControlController, validValidityState} from "../../internal/form";
4
+ import {HasSlotController} from "../../internal/slot";
4
5
  import {property, query, state} from 'lit/decorators.js';
5
6
  import {styleMap} from 'lit/directives/style-map.js';
6
7
  import {unsafeHTML} from "lit/directives/unsafe-html.js";
@@ -22,10 +23,16 @@ import styles from './rating.scss';
22
23
  *
23
24
  * @slot - The default slot.
24
25
  * @slot example - An example slot.
26
+ * @slot help-text - Text that describes how to use the rating. Alternatively, you can use the `help-text` attribute.
25
27
  *
28
+ * @csspart form-control - The form control that wraps the symbols and help text.
29
+ * @csspart form-control-help-text - The help text's wrapper.
26
30
  * @csspart base - The component's base wrapper.
31
+ * @csspart preview - The value shown next to the symbols when `preview` is enabled.
27
32
  *
28
33
  * @cssproperty --example - An example CSS custom property.
34
+ * @cssproperty --preview-color - The color of the preview value.
35
+ * @cssproperty --preview-size - The font size of the preview value.
29
36
  */
30
37
  export default class ZnRating extends ZincElement implements ZincFormControl {
31
38
  static styles: CSSResultGroup = unsafeCSS(styles);
@@ -34,14 +41,21 @@ export default class ZnRating extends ZincElement implements ZincFormControl {
34
41
  assumeInteractionOn: ['zn-blur', 'zn-input']
35
42
  });
36
43
 
44
+ private readonly hasSlotController = new HasSlotController(this, 'help-text');
45
+
37
46
  @query('.rating') rating: HTMLElement;
38
47
 
48
+ @query('.rating__symbols') symbols: HTMLElement;
49
+
39
50
  @state() private hoverValue: number = 0;
40
51
 
41
52
  @state() private isHovering: boolean = false;
42
53
 
43
54
  @property() label: string;
44
55
 
56
+ /** The rating's help text. If you need to display HTML, use the `help-text` slot instead. */
57
+ @property({attribute: 'help-text'}) helpText: string = '';
58
+
45
59
  @property() name: string;
46
60
 
47
61
  @property({type: Number}) value: number = 0;
@@ -54,6 +68,9 @@ export default class ZnRating extends ZincElement implements ZincFormControl {
54
68
 
55
69
  @property({type: Boolean}) disabled: boolean = false;
56
70
 
71
+ /** Displays the value alongside the symbols, updating live as the pointer moves across them. */
72
+ @property({type: Boolean}) preview: boolean = false;
73
+
57
74
  @property({}) size: 'small' | 'medium' | 'large' = 'medium';
58
75
 
59
76
  @property() getSymbol: (value: number) => string = () => '<zn-icon src="star" library="material"></zn-icon>';
@@ -94,12 +111,26 @@ export default class ZnRating extends ZincElement implements ZincFormControl {
94
111
  }
95
112
 
96
113
  private _getValueFromXCoordinate(coordinate: number): number {
97
- const {left, width} = this.rating.getBoundingClientRect();
98
- const value = this._roundToPrecision(((coordinate - left) / width) * this.max, this.precision);
114
+ const {left, width} = this.symbols.getBoundingClientRect();
115
+ // Symbols are spaced with a gap rather than padding, so the symbols only cover part of the
116
+ // container and a plain width-to-value ratio would skew fractions within each symbol.
117
+ const gap = parseFloat(getComputedStyle(this.symbols).columnGap) || 0;
118
+ const symbolWidth = (width - gap * (this.max - 1)) / this.max;
119
+ const stride = symbolWidth + gap;
120
+ const position = Math.min(Math.max(coordinate - left, 0), width);
121
+ const index = Math.min(Math.floor(position / stride), this.max - 1);
122
+ // Landing in a gap reads as the preceding symbol being complete.
123
+ const fraction = Math.min((position - index * stride) / symbolWidth, 1);
124
+ const value = this._roundToPrecision(index + fraction, this.precision);
99
125
 
100
126
  return Math.min(Math.max(value, 0), this.max);
101
127
  }
102
128
 
129
+ private _formatValue(value: number): string {
130
+ const decimals = String(this.precision).split('.')[1]?.length ?? 0;
131
+ return value.toFixed(decimals);
132
+ }
133
+
103
134
  private _getValueFromMousePosition(event: MouseEvent): number {
104
135
  return this._getValueFromXCoordinate(event.clientX);
105
136
  }
@@ -150,13 +181,15 @@ export default class ZnRating extends ZincElement implements ZincFormControl {
150
181
 
151
182
  private _handleTouchEnd(event: TouchEvent) {
152
183
  this.isHovering = false;
153
- this._setValue(this._getValueFromTouchPosition(event));
184
+ // `touches` is empty once the finger lifts, so the final position lives in `changedTouches`.
185
+ this._setValue(this._getValueFromXCoordinate(event.changedTouches[0].clientX));
154
186
 
155
187
  event.preventDefault();
156
188
  }
157
189
 
158
190
  render() {
159
191
  const counter = Array.from(Array(this.max).keys());
192
+ const hasHelpText = this.helpText ? true : this.hasSlotController.test('help-text');
160
193
  let displayValue = 0;
161
194
 
162
195
  if (this.disabled || this.readonly) {
@@ -167,69 +200,102 @@ export default class ZnRating extends ZincElement implements ZincFormControl {
167
200
 
168
201
  return html`
169
202
  <div
170
- part="base"
203
+ part="form-control"
171
204
  class="${classMap({
172
- rating: true,
173
- 'rating--readonly': this.readonly,
174
- 'rating--disabled': this.disabled,
175
- 'rating--small': this.size === 'small',
176
- 'rating--medium': this.size === 'medium',
177
- 'rating--large': this.size === 'large'
178
- })}"
179
- role="slider"
180
- aria-label="${this.label}"
181
- aria-disabled="${this.disabled ? 'true' : 'false'}"
182
- aria-readonly="${this.readonly ? 'true' : 'false'}"
183
- aria-valuenow="${this.value}"
184
- aria-valuemin="0"
185
- aria-valuemax="${this.max}"
186
- @click="${this._handleClick}"
187
- @mouseenter="${this._handleMouseEnter}"
188
- @mousemove="${this._handleMouseMove}"
189
- @mouseleave="${this._handleMouseLeave}"
190
- @touchstart="${this._handleTouchStart}"
191
- @touchmove="${this._handleTouchMove}"
192
- @touceend="${this._handleTouchEnd}"
193
- >
194
- <span class="rating__symbols">
195
- ${counter.map(index => {
196
- if (displayValue > index && displayValue < index + 1) {
205
+ 'form-control': true,
206
+ 'form-control--small': this.size === 'small',
207
+ 'form-control--medium': this.size === 'medium',
208
+ 'form-control--large': this.size === 'large',
209
+ 'form-control--has-help-text': hasHelpText
210
+ })}">
211
+ <div
212
+ part="base"
213
+ class="${classMap({
214
+ rating: true,
215
+ 'rating--readonly': this.readonly,
216
+ 'rating--disabled': this.disabled,
217
+ 'rating--small': this.size === 'small',
218
+ 'rating--medium': this.size === 'medium',
219
+ 'rating--large': this.size === 'large'
220
+ })}"
221
+ role="slider"
222
+ aria-label="${this.label}"
223
+ aria-describedby="help-text"
224
+ aria-disabled="${this.disabled ? 'true' : 'false'}"
225
+ aria-readonly="${this.readonly ? 'true' : 'false'}"
226
+ aria-valuenow="${this.value}"
227
+ aria-valuemin="0"
228
+ aria-valuemax="${this.max}"
229
+ >
230
+ <span
231
+ class="rating__symbols"
232
+ @click="${this._handleClick}"
233
+ @mouseenter="${this._handleMouseEnter}"
234
+ @mousemove="${this._handleMouseMove}"
235
+ @mouseleave="${this._handleMouseLeave}"
236
+ @touchstart="${this._handleTouchStart}"
237
+ @touchmove="${this._handleTouchMove}"
238
+ @touchend="${this._handleTouchEnd}"
239
+ >
240
+ ${counter.map(index => {
241
+ if (displayValue > index && displayValue < index + 1) {
242
+ return html`
243
+ <span
244
+ class=${classMap({
245
+ rating__symbol: true,
246
+ 'rating__partial-symbol-container': true,
247
+ 'rating__symbol--hover': this.isHovering && Math.ceil(displayValue) === index + 1
248
+ })}
249
+ role="presentation">
250
+ <div
251
+ style=${styleMap({
252
+ clipPath: `inset(0 0 0 ${(displayValue - index) * 100}%)`
253
+ })}>
254
+ ${unsafeHTML(this.getSymbol(index + 1))}
255
+ </div>
256
+ <div
257
+ class="rating__partial--filled"
258
+ style=${styleMap({
259
+ clipPath: `inset(0 ${100 - (displayValue - index) * 100}% 0 0)`
260
+ })}>
261
+ ${unsafeHTML(this.getSymbol(index + 1))}
262
+ </div>
263
+ </span>
264
+ `;
265
+ }
197
266
  return html`
198
267
  <span
199
268
  class=${classMap({
200
269
  rating__symbol: true,
201
- 'rating__partial-symbol-container': true,
202
- 'rating__symbol--hover': this.isHovering && Math.ceil(displayValue) === index + 1
270
+ 'rating__symbol--hover': this.isHovering && Math.ceil(displayValue) === index + 1,
271
+ 'rating__symbol--active': displayValue >= index + 1
203
272
  })}
204
273
  role="presentation">
205
- <div
206
- style=${styleMap({
207
- clipPath: `inset(0 0 0 ${(displayValue - index) * 100}%)`
208
- })}>
209
- ${unsafeHTML(this.getSymbol(index + 1))}
210
- </div>
211
- <div
212
- class="rating__partial--filled"
213
- style=${styleMap({
214
- clipPath: `inset(0 ${100 - (displayValue - index) * 100}% 0 0)`
215
- })}>
216
- ${unsafeHTML(this.getSymbol(index + 1))}
217
- </div>
218
- </span>
219
- `;
220
- }
221
- return html`
274
+ ${unsafeHTML(this.getSymbol(index + 1))}
275
+ </span>`;
276
+ })}
277
+ </span>
278
+ ${this.preview
279
+ ? html`
222
280
  <span
281
+ part="preview"
223
282
  class=${classMap({
224
- rating__symbol: true,
225
- 'rating__symbol--hover': this.isHovering && Math.ceil(displayValue) === index + 1,
226
- 'rating__symbol--active': displayValue >= index + 1
283
+ rating__preview: true,
284
+ 'rating__preview--hover': this.isHovering
227
285
  })}
228
- role="presentation">
229
- ${unsafeHTML(this.getSymbol(index + 1))}
230
- </span>`;
231
- })}
232
- </span>
286
+ aria-hidden="true">
287
+ ${this._formatValue(displayValue)}
288
+ </span>`
289
+ : ''}
290
+ </div>
291
+
292
+ <div
293
+ part="form-control-help-text"
294
+ id="help-text"
295
+ class="form-control__help-text"
296
+ aria-hidden=${hasHelpText ? 'false' : 'true'}>
297
+ <slot name="help-text">${this.helpText}</slot>
298
+ </div>
233
299
  </div>
234
300
  `;
235
301
  }
@@ -1,10 +1,13 @@
1
1
  @use "../../wc";
2
+ @use "../../form-control";
2
3
 
3
4
  :host {
4
5
  --symbol-color: rgb(var(--zn-border-color));
5
6
  --symbol-color-active: var(--zn-color-amber-400);
6
7
  --symbol-size: 1.2rem;
7
- --symbol-spacing: 4px;
8
+ --symbol-spacing: var(--zn-spacing-x-small);
9
+ --preview-color: rgb(var(--zn-text));
10
+ --preview-size: 0.875rem;
8
11
 
9
12
  display: block;
10
13
  }
@@ -12,6 +15,7 @@
12
15
  .rating {
13
16
  position: relative;
14
17
  display: inline-flex;
18
+ align-items: center;
15
19
  border-radius: var(--zn-border-radius);
16
20
  vertical-align: middle;
17
21
 
@@ -27,15 +31,12 @@
27
31
  &__symbols {
28
32
  display: inline-flex;
29
33
  position: relative;
34
+ gap: var(--symbol-spacing);
30
35
  font-size: var(--symbol-size);
31
36
  line-height: 0;
32
37
  color: var(--symbol-color);
33
38
  white-space: nowrap;
34
39
  cursor: pointer;
35
-
36
- > * {
37
- padding: var(--symbol-spacing);
38
- }
39
40
  }
40
41
 
41
42
  &__symbol--active,
@@ -49,8 +50,8 @@
49
50
 
50
51
  &__partial--filled {
51
52
  position: absolute;
52
- top: var(--symbol-spacing);
53
- left: var(--symbol-spacing);
53
+ top: 0;
54
+ left: 0;
54
55
  }
55
56
 
56
57
  &__symbol {
@@ -62,6 +63,21 @@
62
63
  }
63
64
  }
64
65
 
66
+ &__preview {
67
+ margin-left: var(--symbol-spacing);
68
+ font-size: var(--preview-size);
69
+ font-weight: var(--zn-font-weight-semibold);
70
+ line-height: 1;
71
+ color: var(--preview-color);
72
+ font-variant-numeric: tabular-nums;
73
+ pointer-events: none;
74
+ user-select: none;
75
+
76
+ &--hover {
77
+ color: var(--symbol-color-active);
78
+ }
79
+ }
80
+
65
81
  &--disabled &__symbols,
66
82
  &--readonly &__symbols {
67
83
  cursor: default;
@@ -72,6 +88,11 @@
72
88
  scale: none;
73
89
  }
74
90
 
91
+ &--disabled &__preview--hover,
92
+ &--readonly &__preview--hover {
93
+ color: var(--preview-color);
94
+ }
95
+
75
96
  &--disabled {
76
97
  opacity: 0.5;
77
98
  }
@@ -82,7 +103,7 @@
82
103
 
83
104
  &--small {
84
105
  --symbol-size: 1rem;
85
- --symbol-spacing: 2px;
106
+ --symbol-spacing: var(--zn-spacing-2x-small);
86
107
  }
87
108
 
88
109
  &--medium {
@@ -91,6 +112,6 @@
91
112
 
92
113
  &--large {
93
114
  --symbol-size: 1.5rem;
94
- --symbol-spacing: 6px;
115
+ --symbol-spacing: 12px;
95
116
  }
96
117
  }