@kubex/zinc 1.1.77 → 1.1.78

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.
@@ -272,6 +329,8 @@ Available CSS custom properties:
272
329
  - `--symbol-color-active` - Color of filled/active symbols
273
330
  - `--symbol-size` - Size of the symbols
274
331
  - `--symbol-spacing` - Spacing between symbols
332
+ - `--preview-color` - Color of the preview value (see [Preview](#preview))
333
+ - `--preview-size` - Font size of the preview value
275
334
 
276
335
  ### Complete Example
277
336
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubex/zinc",
3
- "version": "1.1.77",
3
+ "version": "1.1.78",
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,17 @@ 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();
114
+ const {left, width} = this.symbols.getBoundingClientRect();
98
115
  const value = this._roundToPrecision(((coordinate - left) / width) * this.max, this.precision);
99
116
 
100
117
  return Math.min(Math.max(value, 0), this.max);
101
118
  }
102
119
 
120
+ private _formatValue(value: number): string {
121
+ const decimals = String(this.precision).split('.')[1]?.length ?? 0;
122
+ return value.toFixed(decimals);
123
+ }
124
+
103
125
  private _getValueFromMousePosition(event: MouseEvent): number {
104
126
  return this._getValueFromXCoordinate(event.clientX);
105
127
  }
@@ -150,13 +172,15 @@ export default class ZnRating extends ZincElement implements ZincFormControl {
150
172
 
151
173
  private _handleTouchEnd(event: TouchEvent) {
152
174
  this.isHovering = false;
153
- this._setValue(this._getValueFromTouchPosition(event));
175
+ // `touches` is empty once the finger lifts, so the final position lives in `changedTouches`.
176
+ this._setValue(this._getValueFromXCoordinate(event.changedTouches[0].clientX));
154
177
 
155
178
  event.preventDefault();
156
179
  }
157
180
 
158
181
  render() {
159
182
  const counter = Array.from(Array(this.max).keys());
183
+ const hasHelpText = this.helpText ? true : this.hasSlotController.test('help-text');
160
184
  let displayValue = 0;
161
185
 
162
186
  if (this.disabled || this.readonly) {
@@ -167,69 +191,102 @@ export default class ZnRating extends ZincElement implements ZincFormControl {
167
191
 
168
192
  return html`
169
193
  <div
170
- part="base"
194
+ part="form-control"
171
195
  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) {
196
+ 'form-control': true,
197
+ 'form-control--small': this.size === 'small',
198
+ 'form-control--medium': this.size === 'medium',
199
+ 'form-control--large': this.size === 'large',
200
+ 'form-control--has-help-text': hasHelpText
201
+ })}">
202
+ <div
203
+ part="base"
204
+ class="${classMap({
205
+ rating: true,
206
+ 'rating--readonly': this.readonly,
207
+ 'rating--disabled': this.disabled,
208
+ 'rating--small': this.size === 'small',
209
+ 'rating--medium': this.size === 'medium',
210
+ 'rating--large': this.size === 'large'
211
+ })}"
212
+ role="slider"
213
+ aria-label="${this.label}"
214
+ aria-describedby="help-text"
215
+ aria-disabled="${this.disabled ? 'true' : 'false'}"
216
+ aria-readonly="${this.readonly ? 'true' : 'false'}"
217
+ aria-valuenow="${this.value}"
218
+ aria-valuemin="0"
219
+ aria-valuemax="${this.max}"
220
+ >
221
+ <span
222
+ class="rating__symbols"
223
+ @click="${this._handleClick}"
224
+ @mouseenter="${this._handleMouseEnter}"
225
+ @mousemove="${this._handleMouseMove}"
226
+ @mouseleave="${this._handleMouseLeave}"
227
+ @touchstart="${this._handleTouchStart}"
228
+ @touchmove="${this._handleTouchMove}"
229
+ @touchend="${this._handleTouchEnd}"
230
+ >
231
+ ${counter.map(index => {
232
+ if (displayValue > index && displayValue < index + 1) {
233
+ return html`
234
+ <span
235
+ class=${classMap({
236
+ rating__symbol: true,
237
+ 'rating__partial-symbol-container': true,
238
+ 'rating__symbol--hover': this.isHovering && Math.ceil(displayValue) === index + 1
239
+ })}
240
+ role="presentation">
241
+ <div
242
+ style=${styleMap({
243
+ clipPath: `inset(0 0 0 ${(displayValue - index) * 100}%)`
244
+ })}>
245
+ ${unsafeHTML(this.getSymbol(index + 1))}
246
+ </div>
247
+ <div
248
+ class="rating__partial--filled"
249
+ style=${styleMap({
250
+ clipPath: `inset(0 ${100 - (displayValue - index) * 100}% 0 0)`
251
+ })}>
252
+ ${unsafeHTML(this.getSymbol(index + 1))}
253
+ </div>
254
+ </span>
255
+ `;
256
+ }
197
257
  return html`
198
258
  <span
199
259
  class=${classMap({
200
260
  rating__symbol: true,
201
- 'rating__partial-symbol-container': true,
202
- 'rating__symbol--hover': this.isHovering && Math.ceil(displayValue) === index + 1
261
+ 'rating__symbol--hover': this.isHovering && Math.ceil(displayValue) === index + 1,
262
+ 'rating__symbol--active': displayValue >= index + 1
203
263
  })}
204
264
  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`
265
+ ${unsafeHTML(this.getSymbol(index + 1))}
266
+ </span>`;
267
+ })}
268
+ </span>
269
+ ${this.preview
270
+ ? html`
222
271
  <span
272
+ part="preview"
223
273
  class=${classMap({
224
- rating__symbol: true,
225
- 'rating__symbol--hover': this.isHovering && Math.ceil(displayValue) === index + 1,
226
- 'rating__symbol--active': displayValue >= index + 1
274
+ rating__preview: true,
275
+ 'rating__preview--hover': this.isHovering
227
276
  })}
228
- role="presentation">
229
- ${unsafeHTML(this.getSymbol(index + 1))}
230
- </span>`;
231
- })}
232
- </span>
277
+ aria-hidden="true">
278
+ ${this._formatValue(displayValue)}
279
+ </span>`
280
+ : ''}
281
+ </div>
282
+
283
+ <div
284
+ part="form-control-help-text"
285
+ id="help-text"
286
+ class="form-control__help-text"
287
+ aria-hidden=${hasHelpText ? 'false' : 'true'}>
288
+ <slot name="help-text">${this.helpText}</slot>
289
+ </div>
233
290
  </div>
234
291
  `;
235
292
  }
@@ -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
8
  --symbol-spacing: 4px;
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
 
@@ -62,6 +66,21 @@
62
66
  }
63
67
  }
64
68
 
69
+ &__preview {
70
+ margin-left: var(--symbol-spacing);
71
+ font-size: var(--preview-size);
72
+ font-weight: var(--zn-font-weight-semibold);
73
+ line-height: 1;
74
+ color: var(--preview-color);
75
+ font-variant-numeric: tabular-nums;
76
+ pointer-events: none;
77
+ user-select: none;
78
+
79
+ &--hover {
80
+ color: var(--symbol-color-active);
81
+ }
82
+ }
83
+
65
84
  &--disabled &__symbols,
66
85
  &--readonly &__symbols {
67
86
  cursor: default;
@@ -72,6 +91,11 @@
72
91
  scale: none;
73
92
  }
74
93
 
94
+ &--disabled &__preview--hover,
95
+ &--readonly &__preview--hover {
96
+ color: var(--preview-color);
97
+ }
98
+
75
99
  &--disabled {
76
100
  opacity: 0.5;
77
101
  }