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

IgniteUI / igniteui-webcomponents / 28792371834

06 Jul 2026 12:43PM UTC coverage: 98.375% (+0.04%) from 98.333%
28792371834

Pull #2242

github

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

6258 of 6571 branches covered (95.24%)

Branch coverage included in aggregate %.

2772 of 2793 new or added lines in 12 files covered. (99.25%)

44789 of 45319 relevant lines covered (98.83%)

1872.54 hits per line

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

96.39
/src/components/qr-code/qr-code.ts
1
import { css, html, LitElement, nothing, type PropertyValues } from 'lit';
8✔
2
import { property, state } from 'lit/decorators.js';
8✔
3
import { createAbortHandle } from '../common/abort-handler.js';
8✔
4
import { registerComponent } from '../common/definitions/register.js';
8✔
5
import { bindIf, clamp, nanoid } from '../common/util.js';
8✔
6
import { generateQRCodeMatrix } from './model/matrix.js';
8✔
7
import {
8✔
8
  DEFAULT_SIZE_RATIO,
8✔
9
  DOT_BACKGROUND,
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 type {
8✔
17
  QrCornerSquareStyle,
8✔
18
  QrDotStyle,
8✔
19
  QrErrorCorrectionLevel,
8✔
20
} from './types.js';
8✔
21

8✔
22
/**
8✔
23
 *
8✔
24
 * Generates a QR code based on the provided value and options.
8✔
25
 * The component renders an SVG representation of the QR code, which can be customized using various properties.
8✔
26
 *
8✔
27
 * @element igc-qr-code
8✔
28
 *
8✔
29
 * @cssproperty --igc-qr-dark - The color used for the dark modules of the QR code. Default is #000.
8✔
30
 * @cssproperty --igc-qr-background - The color used for the background of the QR code. Default is #fff.
8✔
31
 * @cssproperty --qr-corner-square-fill - The fill color for the corner squares of the QR code. Default is black.
8✔
32
 * @cssproperty --qr-corner-dot-fill - The fill color for the corner dots of the QR code. Default is black.
8✔
33
 */
8✔
34
export default class IgcQrCodeComponent extends LitElement {
55✔
35
  public static readonly tagName = 'igc-qr-code';
55✔
36

55✔
37
  public static override styles = css`
55✔
38
    :host {
55✔
39
      display: inline-block;
55✔
40
      contain: content;
55✔
41
    }
55✔
42
  `;
55✔
43

55✔
44
  /* blazorSuppress */
55✔
45
  public static register(): void {
55✔
46
    registerComponent(IgcQrCodeComponent);
1✔
47
  }
1✔
48

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

55✔
53
  @state()
55✔
54
  private _logoAspectRatio = 1;
55✔
55

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

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

55✔
76
  /**
55✔
77
   * 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.
55✔
78
   * Valid values are 'L', 'M', 'Q', and 'H', where 'L' provides the lowest level of error correction and 'H' provides the highest level.
55✔
79
   *
55✔
80
   * @attr error-level
55✔
81
   * @default 'M'
55✔
82
   */
55✔
83
  @property({ attribute: 'error-level' })
55✔
84
  public errorLevel?: QrErrorCorrectionLevel = 'M';
55✔
85

55✔
86
  /**
55✔
87
   * 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.
55✔
88
   *
55✔
89
   * @attr size
55✔
90
   * @default 128
55✔
91
   */
55✔
92
  @property({ type: Number })
55✔
93
  public size = 128;
55✔
94

55✔
95
  /**
55✔
96
   * The margin around the QR code in pixels. This is the whitespace area surrounding the QR code,
55✔
97
   * which helps ensure that it can be properly scanned.
55✔
98
   *
55✔
99
   * @attr margin
55✔
100
   * @default 4
55✔
101
   */
55✔
102
  @property({ type: Number })
55✔
103
  public margin = 4;
55✔
104

55✔
105
  /**
55✔
106
   * 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.
55✔
107
   * If provided, the component will attempt to render the logo within the QR code while maintaining scannability.
55✔
108
   *
55✔
109
   * @attr logo-src
55✔
110
   */
55✔
111
  @property({ attribute: 'logo-src' })
55✔
112
  public logoSrc?: string;
55✔
113

55✔
114
  /**
55✔
115
   * 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.
55✔
116
   * 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).
55✔
117
   * The default value is 0.4, meaning the logo will take up 40% of the QR code size.
55✔
118
   *
55✔
119
   * @attr logo-size
55✔
120
   * @default 0.4
55✔
121
   */
55✔
122
  @property({ type: Number, attribute: 'logo-size' })
55✔
123
  public logoSize = 0.4;
55✔
124

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

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

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

8✔
152
  /** @internal */
8✔
153
  protected override update(props: PropertyValues<this>): void {
8✔
154
    if (props.has('logoSrc')) {
79✔
155
      this._resolveAspectRatio();
22✔
156
    }
22✔
157

79✔
158
    super.update(props);
79✔
159
  }
79✔
160

8✔
161
  private _resolveAspectRatio(): void {
8✔
162
    if (!this._hasValidLogoSrc()) {
22✔
163
      this._abortHandle.abort();
8✔
164
      this._logoAspectRatio = 1;
8✔
165
      return;
8✔
166
    }
8✔
167

14✔
168
    this._abortHandle.abort();
14✔
169
    const signal = this._abortHandle.signal;
14✔
170

14✔
171
    const img = new Image();
14✔
172
    img.src = this.logoSrc!;
14✔
173

14✔
174
    if (img.complete && img.naturalWidth && img.naturalHeight) {
22✔
175
      this._logoAspectRatio = img.naturalWidth / img.naturalHeight;
11✔
176
      return;
11✔
177
    }
11✔
178

3✔
179
    this._logoAspectRatio = 1;
3✔
180

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

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

8✔
207
  private _pickErrorLevel(area: number): QrErrorCorrectionLevel {
8✔
NEW
208
    if (area <= SAFE_AREAS.L) return 'L';
×
NEW
209
    if (area <= SAFE_AREAS.M) return 'M';
×
NEW
210
    if (area <= SAFE_AREAS.Q) return 'Q';
×
NEW
211
    return 'H';
×
NEW
212
  }
×
213

8✔
214
  private _getErrorLevelAndArea(hasLogo: boolean) {
8✔
215
    const userErrorLevel = this.errorLevel;
61✔
216
    const size = this.logoSize;
61✔
217
    const sizeRatio = hasLogo ? clamp(size ?? DEFAULT_SIZE_RATIO, 0, 1) : 0;
61✔
218
    const targetArea = sizeRatio * MAX_SAFE_AREA;
61✔
219

61✔
220
    let errorLevel: QrErrorCorrectionLevel;
61✔
221
    let area: number;
61✔
222

61✔
223
    if (userErrorLevel) {
61✔
224
      errorLevel = userErrorLevel;
61✔
225
      area = Math.min(targetArea, SAFE_AREAS[userErrorLevel]);
61✔
226
    } else if (targetArea > 0) {
61!
NEW
227
      errorLevel = this._pickErrorLevel(targetArea);
×
NEW
228
      area = targetArea;
×
NEW
229
    } else {
×
NEW
230
      errorLevel = 'M';
×
NEW
231
      area = 0;
×
NEW
232
    }
×
233

61✔
234
    return { errorLevel, area };
61✔
235
  }
61✔
236

8✔
237
  protected override render() {
8✔
238
    if (!this.value) return nothing;
79✔
239

61✔
240
    const hasLogo = this._hasValidLogoSrc();
61✔
241
    const { errorLevel, area } = this._getErrorLevelAndArea(hasLogo);
61✔
242

61✔
243
    const { matrix, size } = generateQRCodeMatrix(
61✔
244
      this.value,
61✔
245
      errorLevel,
61✔
246
      this.version
61✔
247
    );
61✔
248

61✔
249
    const totalModules = size + this.margin * 2;
61✔
250
    const moduleSize = size / totalModules;
61✔
251
    const marginPx = this.margin * moduleSize;
61✔
252
    const svgSize = moduleSize * (size + this.margin * 2);
61✔
253

61✔
254
    const { mask, image, shouldApplyMask } = renderQrMaskAndImage({
61✔
255
      hasLogo,
61✔
256
      src: this.logoSrc!,
61✔
257
      aspectRatio: this._logoAspectRatio,
61✔
258
      area,
61✔
259
      size: this.size,
61✔
260
      margin: this.logoMargin,
61✔
261
      svgSize,
61✔
262
      maskId: this._maskId,
61✔
263
    });
61✔
264

61✔
265
    return html`
61✔
266
      <svg
61✔
267
        xmlns="http://www.w3.org/2000/svg"
61✔
268
        role="img"
61✔
269
        width=${this.size}
61✔
270
        height=${this.size}
61✔
271
        viewBox="0 0 ${svgSize} ${svgSize}"
61✔
272
      >
61✔
273
        <title>${this.ariaLabel ?? `QR code: ${this.value}`}</title>
79✔
274

79✔
275
        <rect width=${svgSize} height=${svgSize} fill=${DOT_BACKGROUND} />
79✔
276
        ${mask}
79✔
277
        <g mask=${bindIf(shouldApplyMask, this._maskUrl)}>
79✔
278
          ${renderQrDots({
79✔
279
            matrix,
79✔
280
            moduleSize,
79✔
281
            marginPx,
79✔
282
            dotStyle: this.dotStyle,
79✔
283
          })}
79✔
284
          ${renderQrFinders({
79✔
285
            size,
79✔
286
            moduleSize,
79✔
287
            marginPx,
79✔
288
            dotStyle: this.dotStyle,
79✔
289
            squareStyle: this.squareStyle,
79✔
290
          })}
79✔
291
        </g>
79✔
292
        ${image}
79✔
293
      </svg>
79✔
294
    `;
79✔
295
  }
79✔
296
}
8✔
297

8✔
298
declare global {
8✔
299
  interface HTMLElementTagNameMap {
8✔
300
    'igc-qr-code': IgcQrCodeComponent;
8✔
301
  }
8✔
302
}
8✔
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