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

IgniteUI / igniteui-webcomponents / 30915904563

04 Aug 2026 01:51PM UTC coverage: 98.358% (+0.04%) from 98.316%
30915904563

Pull #2242

github

web-flow
Merge d519175cb into e4b484cbf
Pull Request #2242: feat: Added QR code component with encoding and rendering capabilities

6449 of 6780 branches covered (95.12%)

Branch coverage included in aggregate %.

2766 of 2787 new or added lines in 12 files covered. (99.25%)

1 existing line in 1 file now uncovered.

46078 of 46624 relevant lines covered (98.83%)

1907.55 hits per line

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

96.43
/src/components/qr-code/qr-code.ts
1
import { html, LitElement, nothing, type PropertyValues } from 'lit';
8✔
2
import { property, state } from 'lit/decorators.js';
8✔
3
import { addThemingController } from '../../theming/theming-controller.js';
8✔
4
import { createAbortHandle } from '../common/abort-handler.js';
8✔
5
import { registerComponent } from '../common/definitions/register.js';
8✔
6
import { bindIf, clamp, nanoid } from '../common/util.js';
8✔
7
import { generateQRCodeMatrix } from './model/matrix.js';
8✔
8
import {
8✔
9
  DEFAULT_SIZE_RATIO,
8✔
10
  MAX_SAFE_AREA,
8✔
11
  SAFE_AREAS,
8✔
12
} from './renderer/constants.js';
8✔
13
import { renderQrFinders } from './renderer/corner.js';
8✔
14
import { renderQrDots } from './renderer/dots.js';
8✔
15
import { renderQrMaskAndImage } from './renderer/image.js';
8✔
16
import { styles } from './themes/qr-code.base.css.js';
8✔
17
import { styles as shared } from './themes/shared/qr-code.common.css.js';
8✔
18
import { all } from './themes/themes.js';
8✔
19
import type {
8✔
20
  QrCornerSquareStyle,
8✔
21
  QrDotStyle,
8✔
22
  QrErrorCorrectionLevel,
8✔
23
} from './types.js';
8✔
24

8✔
25
/**
8✔
26
 *
8✔
27
 * Generates a QR code based on the provided value and options.
8✔
28
 * The component renders an SVG representation of the QR code, which can be customized using various properties.
8✔
29
 *
8✔
30
 * @element igc-qr-code
8✔
31
 *
8✔
32
 * @csspart background - The background rect of the QR code.
8✔
33
 * @csspart dots - The data modules (dots) of the QR code.
8✔
34
 * @csspart corner-square - The outer corner (finder-pattern) squares of the QR code.
8✔
35
 * @csspart corner-dot - The inner corner (finder-pattern) dots of the QR code.
8✔
36
 */
8✔
37
export default class IgcQrCodeComponent extends LitElement {
8✔
38
  public static readonly tagName = 'igc-qr-code';
8✔
39

8✔
40
  public static override styles = [styles, shared];
8✔
41

8✔
42
  /* blazorSuppress */
8✔
43
  public static register(): void {
8✔
44
    registerComponent(IgcQrCodeComponent);
1✔
45
  }
1✔
46

8✔
47
  private readonly _abortHandle = createAbortHandle();
8✔
48
  private readonly _maskId = nanoid(8);
8✔
49
  private readonly _maskUrl = `url(#${this._maskId})`;
8✔
50

8✔
51
  @state()
8✔
52
  private _logoAspectRatio = 1;
8✔
53

8✔
54
  constructor() {
8✔
55
    super();
61✔
56
    addThemingController(this, all);
61✔
57
  }
61✔
58

8✔
59
  /**
8✔
60
   * The value to be encoded in the QR code. This can be any string, such as a URL, text, or other data.
8✔
61
   * When this property is set, the component will generate a QR code representing the provided value.
8✔
62
   *
8✔
63
   * @attr value
8✔
64
   */
8✔
65
  @property()
8✔
66
  public value?: string;
8✔
67

8✔
68
  /**
8✔
69
   * The version of the QR code to generate, which determines the size and data capacity of the QR code.
8✔
70
   * Valid values are integers from 1 to 40, where each version corresponds to a specific module size and data capacity.
8✔
71
   *
8✔
72
   * If not specified, the component will automatically select the smallest version that can accommodate the provided value.
8✔
73
   *
8✔
74
   * @attr version
8✔
75
   */
8✔
76
  @property({ type: Number })
8✔
77
  public version?: number;
8✔
78

8✔
79
  /**
8✔
80
   * The error correction level for the QR code, which determines the QR code's ability to be read if it is partially obscured or damaged.
8✔
81
   * Valid values are 'L', 'M', 'Q', and 'H', where 'L' provides the lowest level of error correction and 'H' provides the highest level.
8✔
82
   *
8✔
83
   * @attr error-level
8✔
84
   * @default 'M'
8✔
85
   */
8✔
86
  @property({ attribute: 'error-level' })
8✔
87
  public errorLevel?: QrErrorCorrectionLevel = 'M';
8✔
88

8✔
89
  /**
8✔
90
   * The size of the QR code in pixels. This determines the width and height of the generated QR code. The default value is 128 pixels.
8✔
91
   *
8✔
92
   * @attr size
8✔
93
   * @default 128
8✔
94
   */
8✔
95
  @property({ type: Number })
8✔
96
  public size = 128;
8✔
97

8✔
98
  /**
8✔
99
   * The margin (quiet zone) around the QR code, expressed as a number of QR code modules rather
8✔
100
   * than pixels. This is the blank border area surrounding the code, which helps ensure that it
8✔
101
   * can be properly scanned.
8✔
102
   *
8✔
103
   * @attr margin
8✔
104
   * @default 4
8✔
105
   */
8✔
106
  @property({ type: Number })
8✔
107
  public margin = 4;
8✔
108

8✔
109
  /**
8✔
110
   * The source URL of an optional logo image to be displayed at the center of the QR code. The logo can help with branding and recognition.
8✔
111
   * If provided, the component will attempt to render the logo within the QR code while maintaining scannability.
8✔
112
   *
8✔
113
   * @attr logo-src
8✔
114
   */
8✔
115
  @property({ attribute: 'logo-src' })
8✔
116
  public logoSrc?: string;
8✔
117

8✔
118
  /**
8✔
119
   * The size of the logo as a ratio of the QR code size. This determines how large the logo will appear within the QR code.
8✔
120
   * The value should be a number between 0 and 1, where 0 means no logo and 1 means the logo will take up the entire QR code (which is not recommended).
8✔
121
   * The default value is 0.4, meaning the logo will take up 40% of the QR code size.
8✔
122
   *
8✔
123
   * @attr logo-size
8✔
124
   * @default 0.4
8✔
125
   */
8✔
126
  @property({ type: Number, attribute: 'logo-size' })
8✔
127
  public logoSize = 0.4;
8✔
128

8✔
129
  /**
8✔
130
   * The margin around the logo in pixels. This is the whitespace area surrounding the logo within the QR code,
8✔
131
   * which helps ensure that the logo does not interfere with the QR code's scannability.
8✔
132
   *
8✔
133
   * @attr logo-margin
8✔
134
   */
8✔
135
  @property({ type: Number, attribute: 'logo-margin' })
8✔
136
  public logoMargin?: number;
8✔
137

8✔
138
  /**
8✔
139
   * The style of the data modules (dots) in the QR code. This can be 'square', 'circle', or 'rounded'.
8✔
140
   *
8✔
141
   * @attr dot-style
8✔
142
   * @default 'square'
8✔
143
   */
8✔
144
  @property({ attribute: 'dot-style' })
8✔
145
  public dotStyle: QrDotStyle = 'square';
8✔
146

8✔
147
  /**
8✔
148
   * The style of the corner squares in the QR code. This can be 'square', 'circle', or 'rounded'.
8✔
149
   *
8✔
150
   * @attr square-style
8✔
151
   * @default 'square'
8✔
152
   */
8✔
153
  @property({ attribute: 'square-style' })
8✔
154
  public squareStyle: QrCornerSquareStyle = 'square';
8✔
155

8✔
156
  /** @internal */
8✔
157
  protected override update(props: PropertyValues<this>): void {
8✔
158
    if (props.has('logoSrc')) {
86✔
159
      this._resolveAspectRatio();
22✔
160
    }
22✔
161

86✔
162
    super.update(props);
86✔
163
  }
86✔
164

8✔
165
  private _resolveAspectRatio(): void {
8✔
166
    if (!this._hasValidLogoSrc()) {
22✔
167
      this._abortHandle.abort();
8✔
168
      this._logoAspectRatio = 1;
8✔
169
      return;
8✔
170
    }
8✔
171

14✔
172
    this._abortHandle.abort();
14✔
173
    const signal = this._abortHandle.signal;
14✔
174

14✔
175
    const img = new Image();
14✔
176
    img.src = this.logoSrc!;
14✔
177

14✔
178
    if (img.complete && img.naturalWidth && img.naturalHeight) {
22✔
179
      this._logoAspectRatio = img.naturalWidth / img.naturalHeight;
11✔
180
      return;
11✔
181
    }
11✔
182

3✔
183
    this._logoAspectRatio = 1;
3✔
184

3✔
185
    img.addEventListener(
3✔
186
      'load',
3✔
187
      () => {
3✔
188
        if (img.naturalWidth && img.naturalHeight) {
1✔
189
          this._logoAspectRatio = img.naturalWidth / img.naturalHeight;
1✔
190
        }
1✔
191
      },
1✔
192
      { once: true, signal }
3✔
193
    );
3✔
194
  }
22✔
195

8✔
196
  /**
8✔
197
   * Determines whether a valid logo source is provided.
8✔
198
   *
8✔
199
   * The method checks if the `logoSrc` property is set and if it does not start with potentially unsafe schemes like 'javascript:' or 'vbscript:'.
8✔
200
   * It also ensures that if the source is a data URI, it must be an image type.
8✔
201
   * This validation helps prevent security risks associated with rendering untrusted content in the QR code.
8✔
202
   */
8✔
203
  private _hasValidLogoSrc(): boolean {
8✔
204
    if (!this.logoSrc) return false;
90✔
205
    const s = this.logoSrc.trim().toLowerCase();
42✔
206
    if (s.startsWith('javascript:') || s.startsWith('vbscript:')) return false;
90✔
207
    if (s.startsWith('data:') && !s.startsWith('data:image/')) return false;
90✔
208
    return true;
28✔
209
  }
90✔
210

8✔
211
  private _pickErrorLevel(area: number): QrErrorCorrectionLevel {
8✔
NEW
212
    if (area <= SAFE_AREAS.L) return 'L';
×
NEW
213
    if (area <= SAFE_AREAS.M) return 'M';
×
NEW
214
    if (area <= SAFE_AREAS.Q) return 'Q';
×
NEW
215
    return 'H';
×
NEW
216
  }
×
217

8✔
218
  private _getErrorLevelAndArea(hasLogo: boolean) {
8✔
219
    const userErrorLevel = this.errorLevel;
68✔
220
    const size = this.logoSize;
68✔
221
    const sizeRatio = hasLogo ? clamp(size ?? DEFAULT_SIZE_RATIO, 0, 1) : 0;
68✔
222
    const targetArea = sizeRatio * MAX_SAFE_AREA;
68✔
223

68✔
224
    let errorLevel: QrErrorCorrectionLevel;
68✔
225
    let area: number;
68✔
226

68✔
227
    if (userErrorLevel) {
68✔
228
      errorLevel = userErrorLevel;
68✔
229
      area = Math.min(targetArea, SAFE_AREAS[userErrorLevel]);
68✔
230
    } else if (targetArea > 0) {
68!
NEW
231
      errorLevel = this._pickErrorLevel(targetArea);
×
NEW
232
      area = targetArea;
×
NEW
233
    } else {
×
NEW
234
      errorLevel = 'M';
×
NEW
235
      area = 0;
×
NEW
236
    }
×
237

68✔
238
    return { errorLevel, area };
68✔
239
  }
68✔
240

8✔
241
  protected override render() {
8✔
242
    if (!this.value) return nothing;
86✔
243

68✔
244
    const hasLogo = this._hasValidLogoSrc();
68✔
245
    const { errorLevel, area } = this._getErrorLevelAndArea(hasLogo);
68✔
246

68✔
247
    const { matrix, size } = generateQRCodeMatrix(
68✔
248
      this.value,
68✔
249
      errorLevel,
68✔
250
      this.version
68✔
251
    );
68✔
252

68✔
253
    const totalModules = size + this.margin * 2;
68✔
254
    const moduleSize = size / totalModules;
68✔
255
    const marginPx = this.margin * moduleSize;
68✔
256
    const svgSize = moduleSize * (size + this.margin * 2);
68✔
257

68✔
258
    const { mask, image, shouldApplyMask } = renderQrMaskAndImage({
68✔
259
      hasLogo,
68✔
260
      src: this.logoSrc!,
68✔
261
      aspectRatio: this._logoAspectRatio,
68✔
262
      area,
68✔
263
      size: this.size,
68✔
264
      margin: this.logoMargin,
68✔
265
      svgSize,
68✔
266
      maskId: this._maskId,
68✔
267
    });
68✔
268

68✔
269
    return html`
68✔
270
      <svg
68✔
271
        xmlns="http://www.w3.org/2000/svg"
68✔
272
        role="img"
68✔
273
        width=${this.size}
68✔
274
        height=${this.size}
68✔
275
        viewBox="0 0 ${svgSize} ${svgSize}"
68✔
276
      >
68✔
277
        <title>${this.ariaLabel ?? `QR code: ${this.value}`}</title>
86✔
278

86✔
279
        <rect part="background" width=${svgSize} height=${svgSize} />
86✔
280
        ${mask}
86✔
281
        <g mask=${bindIf(shouldApplyMask, this._maskUrl)}>
86✔
282
          ${renderQrDots({
86✔
283
            matrix,
86✔
284
            moduleSize,
86✔
285
            marginPx,
86✔
286
            dotStyle: this.dotStyle,
86✔
287
          })}
86✔
288
          ${renderQrFinders({
86✔
289
            size,
86✔
290
            moduleSize,
86✔
291
            marginPx,
86✔
292
            dotStyle: this.dotStyle,
86✔
293
            squareStyle: this.squareStyle,
86✔
294
          })}
86✔
295
        </g>
86✔
296
        ${image}
86✔
297
      </svg>
86✔
298
    `;
86✔
299
  }
86✔
300
}
8✔
301

8✔
302
declare global {
8✔
303
  interface HTMLElementTagNameMap {
8✔
304
    'igc-qr-code': IgcQrCodeComponent;
8✔
305
  }
8✔
306
}
8✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc