• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IgniteUI / igniteui-webcomponents / 12671556338

08 Jan 2025 01:27PM UTC coverage: 98.277% (+0.008%) from 98.269%
12671556338

Pull #1532

github

web-flow
Merge d5e196207 into db363171b
Pull Request #1532: fix(icon-broadcast): Do not sync between wc services, just with angul…

3864 of 4062 branches covered (95.13%)

Branch coverage included in aggregate %.

8 of 8 new or added lines in 2 files covered. (100.0%)

10 existing lines in 2 files now uncovered.

24776 of 25080 relevant lines covered (98.79%)

469.17 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.91
/src/components/rating/rating.ts
1
import { LitElement, html, nothing } from 'lit';
11✔
2
import {
11✔
3
  property,
11✔
4
  query,
11✔
5
  queryAssignedElements,
11✔
6
  queryAssignedNodes,
11✔
7
  state,
11✔
8
} from 'lit/decorators.js';
11✔
9
import { guard } from 'lit/directives/guard.js';
11✔
10
import { ifDefined } from 'lit/directives/if-defined.js';
11✔
11
import { styleMap } from 'lit/directives/style-map.js';
11✔
12

11✔
13
import { themes } from '../../theming/theming-decorator.js';
11✔
14
import {
11✔
15
  addKeybindings,
11✔
16
  arrowDown,
11✔
17
  arrowLeft,
11✔
18
  arrowRight,
11✔
19
  arrowUp,
11✔
20
  endKey,
11✔
21
  homeKey,
11✔
22
} from '../common/controllers/key-bindings.js';
11✔
23
import { registerComponent } from '../common/definitions/register.js';
11✔
24
import type { Constructor } from '../common/mixins/constructor.js';
11✔
25
import { EventEmitterMixin } from '../common/mixins/event-emitter.js';
11✔
26
import { FormAssociatedMixin } from '../common/mixins/forms/associated.js';
11✔
27
import {
11✔
28
  type FormValue,
11✔
29
  createFormValueState,
11✔
30
  defaultNumberTransformers,
11✔
31
} from '../common/mixins/forms/form-value.js';
11✔
32
import {
11✔
33
  asNumber,
11✔
34
  clamp,
11✔
35
  formatString,
11✔
36
  isEmpty,
11✔
37
  isLTR,
11✔
38
  numberOfDecimals,
11✔
39
  roundPrecise,
11✔
40
} from '../common/util.js';
11✔
41
import IgcIconComponent from '../icon/icon.js';
11✔
42
import IgcRatingSymbolComponent from './rating-symbol.js';
11✔
43
import { styles } from './themes/rating.base.css.js';
11✔
44
import { styles as shared } from './themes/shared/rating.common.css.js';
11✔
45
import { all } from './themes/themes.js';
11✔
46

11✔
47
export interface IgcRatingComponentEventMap {
11✔
48
  igcChange: CustomEvent<number>;
11✔
49
  igcHover: CustomEvent<number>;
11✔
50
}
11✔
51

11✔
52
/**
11✔
53
 * Rating provides insight regarding others' opinions and experiences,
11✔
54
 * and can allow the user to submit a rating of their own
11✔
55
 *
11✔
56
 * @element igc-rating
11✔
57
 *
11✔
58
 * @fires igcChange - Emitted when the value of the control changes.
11✔
59
 * @fires igcHover - Emitted when hover is enabled and the user mouses over a symbol of the rating.
11✔
60
 *
11✔
61
 * @csspart base - The main wrapper which holds all of the rating elements.
11✔
62
 * @csspart label - The label part.
11✔
63
 * @csspart value-label - The value label part.
11✔
64
 * @csspart symbols - A wrapper for all rating symbols.
11✔
65
 * @csspart symbol - The part of the encapsulated default symbol.
11✔
66
 * @csspart full - The part of the encapsulated full symbols.
11✔
67
 * @csspart empty - The part of the encapsulated empty symbols.
11✔
68
 *
11✔
69
 * @cssproperty --symbol-size - The size of the symbols.
11✔
70
 * @cssproperty --symbol-full-color - The color of the filled symbol.
11✔
71
 * @cssproperty --symbol-empty-color - The color of the empty symbol.
11✔
72
 * @cssproperty --symbol-full-filter - The filter(s) used for the filled symbol.
11✔
73
 * @cssproperty --symbol-empty-filter - The filter(s) used for the empty symbol.
11✔
74
 */
11✔
75
@themes(all)
11✔
76
export default class IgcRatingComponent extends FormAssociatedMixin(
11✔
77
  EventEmitterMixin<IgcRatingComponentEventMap, Constructor<LitElement>>(
11✔
78
    LitElement
11✔
79
  )
11✔
80
) {
11✔
81
  public static readonly tagName = 'igc-rating';
11✔
82
  public static styles = [styles, shared];
11✔
83

11✔
84
  /* blazorSuppress */
11✔
85
  public static register() {
11✔
86
    registerComponent(
11✔
87
      IgcRatingComponent,
11✔
88
      IgcIconComponent,
11✔
89
      IgcRatingSymbolComponent
11✔
90
    );
11✔
91
  }
11✔
92

11✔
93
  protected override _formValue: FormValue<number>;
11✔
94

11✔
95
  private _max = 5;
11✔
96
  private _step = 1;
11✔
97
  private _single = false;
11✔
98

11✔
99
  @queryAssignedElements({
11✔
100
    selector: IgcRatingSymbolComponent.tagName,
11✔
101
    slot: 'symbol',
11✔
102
  })
11✔
103
  protected ratingSymbols!: IgcRatingSymbolComponent[];
11✔
104

11✔
105
  @query('[part="symbols"]', true)
11✔
106
  protected container!: HTMLElement;
11✔
107

11✔
108
  @queryAssignedNodes({ slot: 'value-label', flatten: true })
11✔
109
  protected valueLabel!: Node[];
11✔
110

11✔
111
  @state()
11✔
112
  protected hoverValue = -1;
11✔
113

11✔
114
  @state()
11✔
115
  protected hoverState = false;
11✔
116

11✔
117
  protected get isInteractive() {
11✔
118
    return !(this.readOnly || this.disabled);
123✔
119
  }
123✔
120

11✔
121
  protected get hasProjectedSymbols() {
11✔
122
    return this.ratingSymbols.length > 0;
103✔
123
  }
103✔
124

11✔
125
  protected get valueText() {
11✔
126
    // Skip IEEE 754 representation for screen readers
89✔
127
    const value = this.round(this.value);
89✔
128
    return this.valueFormat
89✔
129
      ? formatString(this.valueFormat, value, this.max)
2✔
130
      : `${value} of ${this.max}`;
87✔
131
  }
89✔
132

11✔
133
  /**
11✔
134
   * The maximum value for the rating.
11✔
135
   *
11✔
136
   * If there are projected symbols, the maximum value will be resolved
11✔
137
   * based on the number of symbols.
11✔
138
   * @attr max
11✔
139
   * @default 5
11✔
140
   */
11✔
141
  @property({ type: Number })
11✔
142
  public set max(value: number) {
11✔
143
    this._max = this.hasProjectedSymbols
11✔
144
      ? this.ratingSymbols.length
4✔
145
      : Math.max(0, value);
7✔
146

11✔
147
    if (this._max < this.value) {
11✔
148
      this.value = this._max;
1✔
149
    }
1✔
150
  }
11✔
151

11✔
152
  public get max(): number {
11✔
153
    return this._max;
1,069✔
154
  }
1,069✔
155

11✔
156
  /**
11✔
157
   * The minimum value change allowed.
11✔
158
   *
11✔
159
   * Valid values are in the interval between 0 and 1 inclusive.
11✔
160
   * @attr step
11✔
161
   * @default 1
11✔
162
   */
11✔
163
  @property({ type: Number })
11✔
164
  public set step(value: number) {
11✔
165
    this._step = this.single ? 1 : clamp(value, 0.001, 1);
8✔
166
  }
8✔
167

11✔
168
  public get step(): number {
11✔
169
    return this._step;
324✔
170
  }
324✔
171

11✔
172
  /**
11✔
173
   * The label of the control.
11✔
174
   * @attr label
11✔
175
   */
11✔
176
  @property()
11✔
177
  public label!: string;
11✔
178

11✔
179
  /**
11✔
180
   * A format string which sets aria-valuetext. Instances of '{0}' will be replaced
11✔
181
   * with the current value of the control and instances of '{1}' with the maximum value for the control.
11✔
182
   *
11✔
183
   * Important for screen-readers and useful for localization.
11✔
184
   * @attr value-format
11✔
185
   */
11✔
186
  @property({ attribute: 'value-format' })
11✔
187
  public valueFormat!: string;
11✔
188

11✔
189
  /* @tsTwoWayProperty(true, "igcChange", "detail", false) */
11✔
190
  /**
11✔
191
   * The current value of the component
11✔
192
   * @attr value
11✔
193
   * @default 0
11✔
194
   */
11✔
195
  @property({ type: Number })
11✔
196
  public set value(number: number) {
11✔
197
    const value = this.hasUpdated
55✔
198
      ? clamp(asNumber(number), 0, this.max)
42✔
199
      : Math.max(asNumber(number), 0);
13✔
200
    this._formValue.setValueAndFormState(value);
55✔
201
    this._validate();
55✔
202
  }
55✔
203

11✔
204
  public get value(): number {
11✔
205
    return this._formValue.value;
1,541✔
206
  }
1,541✔
207

11✔
208
  /**
11✔
209
   * Sets hover preview behavior for the component
11✔
210
   * @attr hover-preview
11✔
211
   */
11✔
212
  @property({ type: Boolean, reflect: true, attribute: 'hover-preview' })
11✔
213
  public hoverPreview = false;
11✔
214

11✔
215
  /**
11✔
216
   * Makes the control a readonly field.
11✔
217
   * @attr readonly
11✔
218
   */
11✔
219
  @property({ type: Boolean, reflect: true, attribute: 'readonly' })
11✔
220
  public readOnly = false;
11✔
221

11✔
222
  /**
11✔
223
   * Toggles single selection visual mode.
11✔
224
   * @attr single
11✔
225
   * @default false
11✔
226
   */
11✔
227
  @property({ type: Boolean, reflect: true })
11✔
228
  public set single(value: boolean) {
11✔
229
    this._single = Boolean(value);
1✔
230

1✔
231
    if (this._single) {
1✔
232
      this.step = 1;
1✔
233
      this.value = Math.ceil(this.value);
1✔
234
    }
1✔
235
  }
1✔
236

11✔
237
  public get single(): boolean {
11✔
238
    return this._single;
730✔
239
  }
730✔
240

11✔
241
  /**
11✔
242
   * Whether to reset the rating when the user selects the same value.
11✔
243
   * @attr allow-reset
11✔
244
   */
11✔
245
  @property({ type: Boolean, reflect: true, attribute: 'allow-reset' })
11✔
246
  public allowReset = false;
11✔
247

11✔
248
  constructor() {
11✔
249
    super();
46✔
250

46✔
251
    this._formValue = createFormValueState(this, {
46✔
252
      initialValue: 0,
46✔
253
      transformers: defaultNumberTransformers,
46✔
254
    });
46✔
255

46✔
256
    addKeybindings(this, {
46✔
257
      skip: () => !this.isInteractive,
46✔
258
      bindingDefaults: { preventDefault: true },
46✔
259
    })
46✔
260
      .set(arrowUp, () => this.emitValueUpdate(this.value + this.step))
46✔
261
      .set(arrowRight, () =>
46✔
262
        this.emitValueUpdate(
5✔
263
          isLTR(this) ? this.value + this.step : this.value - this.step
5✔
264
        )
5✔
265
      )
46✔
266
      .set(arrowDown, () => this.emitValueUpdate(this.value - this.step))
46✔
267
      .set(arrowLeft, () =>
46✔
268
        this.emitValueUpdate(
4✔
269
          isLTR(this) ? this.value - this.step : this.value + this.step
4✔
270
        )
4✔
271
      )
46✔
272
      .set(homeKey, () => this.emitValueUpdate(this.step))
46✔
273
      .set(endKey, () => this.emitValueUpdate(this.max));
46✔
274
  }
46✔
275

11✔
276
  protected override async firstUpdated() {
11✔
277
    await this.updateComplete;
46✔
278
    this._formValue.setValueAndFormState(clamp(this.value, 0, this.max));
46✔
279
    this._pristine = true;
46✔
280
  }
46✔
281

11✔
282
  protected handleClick({ clientX }: PointerEvent) {
11✔
283
    const value = this.calcNewValue(clientX);
4✔
284
    const sameValue = this.value === value;
4✔
285

4✔
286
    if (this.allowReset && sameValue) {
4✔
287
      this.emitValueUpdate(0);
1✔
288
    } else if (!sameValue) {
4✔
289
      this.emitValueUpdate(value);
2✔
290
    }
2✔
291
  }
4✔
292

11✔
293
  protected handlePointerMove({ clientX }: PointerEvent) {
11✔
294
    const value = this.calcNewValue(clientX);
1✔
295

1✔
296
    if (this.hoverValue !== value) {
1✔
297
      // Since pointermove spams a lot, only emit on a value change
1✔
298
      this.hoverValue = value;
1✔
299
      this.emitEvent('igcHover', { detail: this.hoverValue });
1✔
300
    }
1✔
301
  }
1✔
302

11✔
303
  protected emitValueUpdate(value: number) {
11✔
304
    this.value = clamp(value, 0, this.max);
18✔
305
    if (value === this.value) {
18✔
306
      this.emitEvent('igcChange', { detail: this.value });
13✔
307
    }
13✔
308
  }
18✔
309

11✔
310
  protected handleSlotChange() {
11✔
311
    if (this.hasProjectedSymbols) {
3✔
312
      this.max = this.ratingSymbols.length;
3✔
313
      this.requestUpdate();
3✔
314
    }
3✔
315
  }
3✔
316

11✔
317
  protected handleHoverEnabled() {
11✔
UNCOV
318
    this.hoverState = true;
×
UNCOV
319
  }
×
320

11✔
321
  protected handleHoverDisabled() {
11✔
UNCOV
322
    this.hoverState = false;
×
UNCOV
323
  }
×
324

11✔
325
  protected calcNewValue(x: number) {
11✔
326
    const { width, left, right } = this.container.getBoundingClientRect();
5✔
327
    const percent = isLTR(this) ? (x - left) / width : (right - x) / width;
5!
328
    const value = this.round(this.max * percent + this.step / 2);
5✔
329

5✔
330
    return clamp(value, this.step, this.max);
5✔
331
  }
5✔
332

11✔
333
  protected round(value: number) {
11✔
334
    return roundPrecise(value, numberOfDecimals(this.step));
97✔
335
  }
97✔
336

11✔
337
  protected clipSymbol(index: number, isLTR = true) {
11✔
338
    const value = this.hoverState ? this.hoverValue : this.value;
492!
339
    const progress = index + 1 - value;
492✔
340
    const exclusive = progress === 0 || this.value === index + 1 ? 0 : 1;
492✔
341
    const selection = this.single ? exclusive : progress;
492✔
342
    const activate = (p: number) => clamp(p * 100, 0, 100);
492✔
343

492✔
344
    const forward = `inset(0 ${activate(
492✔
345
      isLTR ? selection : 1 - selection
492✔
346
    )}% 0 0)`;
492✔
347
    const backward = `inset(0 0 0 ${activate(
492✔
348
      isLTR ? 1 - selection : selection
492✔
349
    )}%)`;
492✔
350

492✔
351
    return {
492✔
352
      backward: isLTR ? backward : forward,
492✔
353
      forward: isLTR ? forward : backward,
492✔
354
    };
492✔
355
  }
492✔
356

11✔
357
  /**
11✔
358
   * Increments the value of the control by `n` steps multiplied by the
11✔
359
   * step factor.
11✔
360
   */
11✔
361
  public stepUp(n = 1) {
11✔
362
    this.value += this.round(n * this.step);
2✔
363
  }
2✔
364

11✔
365
  /**
11✔
366
   * Decrements the value of the control by `n` steps multiplied by
11✔
367
   * the step factor.
11✔
368
   */
11✔
369
  public stepDown(n = 1) {
11✔
370
    this.value -= this.round(n * this.step);
1✔
371
  }
1✔
372

11✔
373
  protected *renderSymbols() {
11✔
374
    const ltr = isLTR(this);
86✔
375
    for (let i = 0; i < this.max; i++) {
86✔
376
      const { forward, backward } = this.clipSymbol(i, ltr);
483✔
377
      yield html`<igc-rating-symbol exportparts="symbol, full, empty">
483✔
378
        <igc-icon
483✔
379
          collection="default"
483✔
380
          name="star_filled"
483✔
381
          style=${styleMap({ clipPath: forward })}
483✔
382
        ></igc-icon>
483✔
383
        <igc-icon
483✔
384
          collection="default"
483✔
385
          name="star_outlined"
483✔
386
          style=${styleMap({ clipPath: backward })}
483✔
387
          slot="empty"
483✔
388
        ></igc-icon>
483✔
389
      </igc-rating-symbol>`;
483✔
390
    }
483✔
391
  }
86✔
392

11✔
393
  protected clipProjected() {
11✔
394
    const ltr = isLTR(this);
3✔
395
    const partFull = '[part="symbol full"]';
3✔
396
    const partEmpty = '[part="symbol empty"]';
3✔
397

3✔
398
    for (const [i, symbol] of this.ratingSymbols.entries()) {
3✔
399
      const full = symbol.renderRoot.querySelector<HTMLElement>(partFull);
9✔
400
      const empty = symbol.renderRoot.querySelector<HTMLElement>(partEmpty);
9✔
401
      const { forward, backward } = this.clipSymbol(i, ltr);
9✔
402

9✔
403
      if (full) {
9✔
404
        full.style.clipPath = forward;
9✔
405
      }
9✔
406

9✔
407
      if (empty) {
9✔
408
        empty.style.clipPath = backward;
9✔
409
      }
9✔
410
    }
9✔
411
  }
3✔
412

11✔
413
  protected override render() {
11✔
414
    const props = [
89✔
415
      this.value,
89✔
416
      this.hoverValue,
89✔
417
      this.max,
89✔
418
      this.step,
89✔
419
      this.single,
89✔
420
      this.hoverState,
89✔
421
      this.ratingSymbols,
89✔
422
    ];
89✔
423

89✔
424
    const hoverActive = this.hoverPreview && this.isInteractive;
89✔
425

89✔
426
    return html`
89✔
427
      <label part="label" id="rating-label" ?hidden=${!this.label}
89✔
428
        >${this.label}</label
89✔
429
      >
89✔
430
      <div
89✔
431
        part="base"
89✔
432
        role="slider"
89✔
433
        tabindex=${ifDefined(this.disabled ? undefined : 0)}
89✔
434
        aria-labelledby="rating-label"
89✔
435
        aria-valuemin="0"
89✔
436
        aria-valuenow=${this.value}
89✔
437
        aria-valuemax=${this.max}
89✔
438
        aria-valuetext=${this.valueText}
89✔
439
      >
89✔
440
        <div
89✔
441
          aria-hidden="true"
89✔
442
          part="symbols"
89✔
443
          @click=${this.isInteractive ? this.handleClick : nothing}
89✔
444
          @pointerenter=${hoverActive ? this.handleHoverEnabled : nothing}
89✔
445
          @pointerleave=${hoverActive ? this.handleHoverDisabled : nothing}
89✔
446
          @pointermove=${hoverActive ? this.handlePointerMove : nothing}
89✔
447
        >
89✔
448
          <slot name="symbol" @slotchange=${this.handleSlotChange}>
89✔
449
            ${guard(props, () =>
89✔
450
              this.hasProjectedSymbols
89✔
451
                ? this.clipProjected()
3✔
452
                : this.renderSymbols()
86✔
453
            )}
89✔
454
          </slot>
89✔
455
        </div>
89✔
456
        <label part="value-label" ?hidden=${isEmpty(this.valueLabel)}>
89✔
457
          <slot name="value-label"></slot>
89✔
458
        </label>
89✔
459
      </div>
89✔
460
    `;
89✔
461
  }
89✔
462
}
11✔
463

11✔
464
declare global {
11✔
465
  interface HTMLElementTagNameMap {
11✔
466
    'igc-rating': IgcRatingComponent;
11✔
467
  }
11✔
468
}
11✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc