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

IgniteUI / igniteui-webcomponents / 30921987612

04 Aug 2026 03:01PM UTC coverage: 98.348% (+0.03%) from 98.316%
30921987612

Pull #2242

github

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

6455 of 6788 branches covered (95.09%)

Branch coverage included in aggregate %.

2810 of 2835 new or added lines in 12 files covered. (99.12%)

1 existing line in 1 file now uncovered.

46122 of 46672 relevant lines covered (98.82%)

1828.37 hits per line

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

95.41
/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 type { QRCodeMatrixResult } from './model/matrix.js';
8✔
8
import { generateQRCodeMatrix } from './model/matrix.js';
8✔
9
import {
8✔
10
  DEFAULT_SIZE_RATIO,
8✔
11
  MAX_SAFE_AREA,
8✔
12
  SAFE_AREAS,
8✔
13
} from './renderer/constants.js';
8✔
14
import { renderQrFinders } from './renderer/corner.js';
8✔
15
import { renderQrDots } from './renderer/dots.js';
8✔
16
import { renderQrMaskAndImage } from './renderer/image.js';
8✔
17
import { styles } from './themes/qr-code.base.css.js';
8✔
18
import { styles as shared } from './themes/shared/qr-code.common.css.js';
8✔
19
import { all } from './themes/themes.js';
8✔
20
import type {
8✔
21
  QrCornerSquareStyle,
8✔
22
  QrDotStyle,
8✔
23
  QrErrorCorrectionLevel,
8✔
24
} from './types.js';
8✔
25

8✔
26
/**
8✔
27
 *
8✔
28
 * Generates a QR code based on the provided value and options.
8✔
29
 * The component renders an SVG representation of the QR code, which can be customized using various properties.
8✔
30
 *
8✔
31
 * @element igc-qr-code
8✔
32
 *
8✔
33
 * @cssproperty --ig-qr-code-background - The background color of the QR code. Default is `white`.
8✔
34
 * @cssproperty --ig-qr-code-dark-color - The color of the data modules (dots), and the corner square/dot colors unless overridden below. Default is `black`.
8✔
35
 * @cssproperty --ig-qr-code-corner-square-color - The color of the outer finder-pattern corner squares. Defaults to `--ig-qr-code-dark-color`.
8✔
36
 * @cssproperty --ig-qr-code-corner-dot-color - The color of the inner finder-pattern corner dots. Defaults to `--ig-qr-code-dark-color`.
8✔
37
 *
8✔
38
 * @csspart background - The background rect of the QR code.
8✔
39
 * @csspart dots - The data modules (dots) of the QR code.
8✔
40
 * @csspart corner-square - The outer corner (finder-pattern) squares of the QR code.
8✔
41
 * @csspart corner-dot - The inner corner (finder-pattern) dots of the QR code.
8✔
42
 */
8✔
43
export default class IgcQrCodeComponent extends LitElement {
8✔
44
  public static readonly tagName = 'igc-qr-code';
8✔
45

8✔
46
  public static override styles = [styles, shared];
8✔
47

8✔
48
  /* blazorSuppress */
8✔
49
  public static register(): void {
8✔
50
    registerComponent(IgcQrCodeComponent);
1✔
51
  }
1✔
52

8✔
53
  private readonly _abortHandle = createAbortHandle();
8✔
54
  private readonly _maskId = nanoid(8);
8✔
55
  private readonly _maskUrl = `url(#${this._maskId})`;
8✔
56
  private _matrixCache?: {
8✔
57
    value: string;
8✔
58
    errorLevel: QrErrorCorrectionLevel;
8✔
59
    version?: number;
8✔
60
    result: QRCodeMatrixResult;
8✔
61
  };
8✔
62

8✔
63
  @state()
8✔
64
  private _logoAspectRatio = 1;
8✔
65

8✔
66
  @state()
8✔
67
  private _logoLoadFailed = false;
8✔
68

8✔
69
  constructor() {
8✔
70
    super();
63✔
71
    addThemingController(this, all);
63✔
72
  }
63✔
73

8✔
74
  /**
8✔
75
   * The value to be encoded in the QR code. This can be any string, such as a URL, text, or other data.
8✔
76
   * When this property is set, the component will generate a QR code representing the provided value.
8✔
77
   *
8✔
78
   * @attr value
8✔
79
   */
8✔
80
  @property()
8✔
81
  public value?: string;
8✔
82

8✔
83
  /**
8✔
84
   * The version of the QR code to generate, which determines the size and data capacity of the QR code.
8✔
85
   * Valid values are integers from 1 to 40, where each version corresponds to a specific module size and data capacity.
8✔
86
   *
8✔
87
   * If not specified, the component will automatically select the smallest version that can accommodate the provided value.
8✔
88
   *
8✔
89
   * @attr version
8✔
90
   */
8✔
91
  @property({ type: Number })
8✔
92
  public version?: number;
8✔
93

8✔
94
  /**
8✔
95
   * 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✔
96
   * Valid values are 'L', 'M', 'Q', and 'H', where 'L' provides the lowest level of error correction and 'H' provides the highest level.
8✔
97
   *
8✔
98
   * @attr error-level
8✔
99
   * @default 'M'
8✔
100
   */
8✔
101
  @property({ attribute: 'error-level' })
8✔
102
  public errorLevel?: QrErrorCorrectionLevel = 'M';
8✔
103

8✔
104
  /**
8✔
105
   * 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✔
106
   *
8✔
107
   * @attr size
8✔
108
   * @default 128
8✔
109
   */
8✔
110
  @property({ type: Number })
8✔
111
  public size = 128;
8✔
112

8✔
113
  /**
8✔
114
   * The margin (quiet zone) around the QR code, expressed as a number of QR code modules rather
8✔
115
   * than pixels. This is the blank border area surrounding the code, which helps ensure that it
8✔
116
   * can be properly scanned.
8✔
117
   *
8✔
118
   * @attr margin
8✔
119
   * @default 4
8✔
120
   */
8✔
121
  @property({ type: Number })
8✔
122
  public margin = 4;
8✔
123

8✔
124
  /**
8✔
125
   * 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✔
126
   * If provided, the component will attempt to render the logo within the QR code while maintaining scannability.
8✔
127
   *
8✔
128
   * @attr logo-src
8✔
129
   */
8✔
130
  @property({ attribute: 'logo-src' })
8✔
131
  public logoSrc?: string;
8✔
132

8✔
133
  /**
8✔
134
   * The size of the logo, as a ratio of the maximum area that can safely be obscured by a logo
8✔
135
   * while the QR code remains scannable (up to 9% of the code's area, at the highest error
8✔
136
   * correction level). The value should be a number between 0 and 1, where 0 means no logo and 1
8✔
137
   * means the logo will cover the full safe area (not the entire QR code).
8✔
138
   * The default value is 0.4, meaning the logo covers 40% of that safe area (~3.6% of the QR code).
8✔
139
   *
8✔
140
   * When `error-level` is not explicitly set, the smallest error correction level that can
8✔
141
   * accommodate the requested logo size is chosen automatically.
8✔
142
   *
8✔
143
   * @attr logo-size
8✔
144
   * @default 0.4
8✔
145
   */
8✔
146
  @property({ type: Number, attribute: 'logo-size' })
8✔
147
  public logoSize = 0.4;
8✔
148

8✔
149
  /**
8✔
150
   * The margin around the logo in pixels. This is the whitespace area surrounding the logo within the QR code,
8✔
151
   * which helps ensure that the logo does not interfere with the QR code's scannability.
8✔
152
   *
8✔
153
   * @attr logo-margin
8✔
154
   */
8✔
155
  @property({ type: Number, attribute: 'logo-margin' })
8✔
156
  public logoMargin?: number;
8✔
157

8✔
158
  /**
8✔
159
   * The style of the data modules (dots) in the QR code, and of the inner dot of each finder-pattern
8✔
160
   * corner. This can be 'square', 'circle', or 'rounded'.
8✔
161
   *
8✔
162
   * @attr dot-style
8✔
163
   * @default 'square'
8✔
164
   */
8✔
165
  @property({ attribute: 'dot-style' })
8✔
166
  public dotStyle: QrDotStyle = 'square';
8✔
167

8✔
168
  /**
8✔
169
   * The style of the corner squares in the QR code. This can be 'square', 'circle', or 'rounded'.
8✔
170
   *
8✔
171
   * @attr square-style
8✔
172
   * @default 'square'
8✔
173
   */
8✔
174
  @property({ attribute: 'square-style' })
8✔
175
  public squareStyle: QrCornerSquareStyle = 'square';
8✔
176

8✔
177
  /** @internal */
8✔
178
  protected override update(props: PropertyValues<this>): void {
8✔
179
    if (props.has('logoSrc')) {
93✔
180
      this._resolveAspectRatio();
25✔
181
    }
25✔
182

93✔
183
    super.update(props);
93✔
184
  }
93✔
185

8✔
186
  private _resolveAspectRatio(): void {
8✔
187
    this._abortHandle.abort();
25✔
188
    this._logoLoadFailed = false;
25✔
189
    this._logoAspectRatio = 1;
25✔
190

25✔
191
    if (!this._hasValidLogoSrc()) {
25✔
192
      return;
8✔
193
    }
8✔
194

17✔
195
    const signal = this._abortHandle.signal;
17✔
196
    const img = new Image();
17✔
197
    img.src = this.logoSrc!;
17✔
198

17✔
199
    if (img.complete) {
25✔
200
      if (img.naturalWidth && img.naturalHeight) {
12✔
201
        this._logoAspectRatio = img.naturalWidth / img.naturalHeight;
12✔
202
      } else {
12!
NEW
203
        this._logoLoadFailed = true;
×
NEW
204
      }
×
205
      return;
12✔
206
    }
12✔
207

5✔
208
    img.addEventListener(
5✔
209
      'load',
5✔
210
      () => {
5✔
211
        if (img.naturalWidth && img.naturalHeight) {
1✔
212
          this._logoAspectRatio = img.naturalWidth / img.naturalHeight;
1✔
213
        } else {
1!
NEW
214
          this._logoLoadFailed = true;
×
NEW
215
        }
×
216
      },
1✔
217
      { once: true, signal }
5✔
218
    );
5✔
219

5✔
220
    img.addEventListener(
5✔
221
      'error',
5✔
222
      () => {
5✔
223
        this._logoLoadFailed = true;
4✔
224
      },
4✔
225
      { once: true, signal }
5✔
226
    );
5✔
227
  }
25✔
228

8✔
229
  /**
8✔
230
   * Determines whether a valid logo source is provided.
8✔
231
   *
8✔
232
   * The method checks if the `logoSrc` property is set and if it does not start with potentially unsafe schemes like 'javascript:' or 'vbscript:'.
8✔
233
   * It also ensures that if the source is a data URI, it must be an image type.
8✔
234
   * This validation helps prevent security risks associated with rendering untrusted content in the QR code.
8✔
235
   */
8✔
236
  private _hasValidLogoSrc(): boolean {
8✔
237
    if (!this.logoSrc) return false;
99✔
238
    const s = this.logoSrc.trim().toLowerCase();
51✔
239
    if (s.startsWith('javascript:') || s.startsWith('vbscript:')) return false;
99✔
240
    if (s.startsWith('data:') && !s.startsWith('data:image/')) return false;
99✔
241
    return true;
37✔
242
  }
99✔
243

8✔
244
  private _pickErrorLevel(area: number): QrErrorCorrectionLevel {
8✔
NEW
245
    if (area <= SAFE_AREAS.L) return 'L';
×
NEW
246
    if (area <= SAFE_AREAS.M) return 'M';
×
NEW
247
    if (area <= SAFE_AREAS.Q) return 'Q';
×
NEW
248
    return 'H';
×
NEW
249
  }
×
250

8✔
251
  private _getErrorLevelAndArea(hasLogo: boolean) {
8✔
252
    const userErrorLevel = this.errorLevel;
74✔
253
    const size = this.logoSize;
74✔
254
    const sizeRatio = hasLogo ? clamp(size ?? DEFAULT_SIZE_RATIO, 0, 1) : 0;
74✔
255
    const targetArea = sizeRatio * MAX_SAFE_AREA;
74✔
256

74✔
257
    let errorLevel: QrErrorCorrectionLevel;
74✔
258
    let area: number;
74✔
259

74✔
260
    if (userErrorLevel) {
74✔
261
      errorLevel = userErrorLevel;
74✔
262
      area = Math.min(targetArea, SAFE_AREAS[userErrorLevel]);
74✔
263
    } else if (targetArea > 0) {
74!
NEW
264
      errorLevel = this._pickErrorLevel(targetArea);
×
NEW
265
      area = targetArea;
×
NEW
266
    } else {
×
NEW
267
      errorLevel = 'M';
×
NEW
268
      area = 0;
×
NEW
269
    }
×
270

74✔
271
    return { errorLevel, area };
74✔
272
  }
74✔
273

8✔
274
  private _getMatrix(
8✔
275
    value: string,
74✔
276
    errorLevel: QrErrorCorrectionLevel
74✔
277
  ): QRCodeMatrixResult {
74✔
278
    const cached = this._matrixCache;
74✔
279
    if (
74✔
280
      cached &&
74✔
281
      cached.value === value &&
28✔
282
      cached.errorLevel === errorLevel &&
26✔
283
      cached.version === this.version
26✔
284
    ) {
74✔
285
      return cached.result;
26✔
286
    }
26✔
287

48✔
288
    const result = generateQRCodeMatrix(value, errorLevel, this.version);
48✔
289
    this._matrixCache = { value, errorLevel, version: this.version, result };
48✔
290
    return result;
48✔
291
  }
74✔
292

8✔
293
  protected override render() {
8✔
294
    if (!this.value) return nothing;
93✔
295

74✔
296
    const hasLogo = this._hasValidLogoSrc() && !this._logoLoadFailed;
93✔
297
    const { errorLevel, area } = this._getErrorLevelAndArea(hasLogo);
93✔
298

93✔
299
    const { matrix, size } = this._getMatrix(this.value, errorLevel);
93✔
300

93✔
301
    const totalModules = size + this.margin * 2;
93✔
302
    const moduleSize = size / totalModules;
93✔
303
    const marginPx = this.margin * moduleSize;
93✔
304
    const svgSize = moduleSize * (size + this.margin * 2);
93✔
305

93✔
306
    const { mask, image, shouldApplyMask } = renderQrMaskAndImage({
93✔
307
      hasLogo,
93✔
308
      src: this.logoSrc!,
93✔
309
      aspectRatio: this._logoAspectRatio,
93✔
310
      area,
93✔
311
      size: this.size,
93✔
312
      margin: this.logoMargin,
93✔
313
      svgSize,
93✔
314
      maskId: this._maskId,
93✔
315
    });
93✔
316

93✔
317
    return html`
93✔
318
      <svg
93✔
319
        xmlns="http://www.w3.org/2000/svg"
93✔
320
        role="img"
93✔
321
        width=${this.size}
93✔
322
        height=${this.size}
93✔
323
        viewBox="0 0 ${svgSize} ${svgSize}"
93✔
324
      >
93✔
325
        <title>${this.ariaLabel ?? `QR code: ${this.value}`}</title>
93✔
326

93✔
327
        <rect part="background" width=${svgSize} height=${svgSize} />
93✔
328
        ${mask}
93✔
329
        <g mask=${bindIf(shouldApplyMask, this._maskUrl)}>
93✔
330
          ${renderQrDots({
93✔
331
            matrix,
93✔
332
            moduleSize,
93✔
333
            marginPx,
93✔
334
            dotStyle: this.dotStyle,
93✔
335
          })}
93✔
336
          ${renderQrFinders({
93✔
337
            size,
93✔
338
            moduleSize,
93✔
339
            marginPx,
93✔
340
            dotStyle: this.dotStyle,
93✔
341
            squareStyle: this.squareStyle,
93✔
342
          })}
93✔
343
        </g>
93✔
344
        ${image}
93✔
345
      </svg>
93✔
346
    `;
93✔
347
  }
93✔
348
}
8✔
349

8✔
350
declare global {
8✔
351
  interface HTMLElementTagNameMap {
8✔
352
    'igc-qr-code': IgcQrCodeComponent;
8✔
353
  }
8✔
354
}
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