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

IgniteUI / igniteui-webcomponents / 30909291507

04 Aug 2026 12:29PM UTC coverage: 98.36% (+0.04%) from 98.316%
30909291507

Pull #2242

github

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

6450 of 6781 branches covered (95.12%)

Branch coverage included in aggregate %.

2778 of 2799 new or added lines in 12 files covered. (99.25%)

46091 of 46636 relevant lines covered (98.83%)

1864.68 hits per line

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

96.4
/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 (quiet zone) around the QR code, expressed as a number of QR code modules rather
55✔
97
   * than pixels. This is the blank border area surrounding the code, which helps ensure that it
55✔
98
   * can be properly scanned.
55✔
99
   *
55✔
100
   * @attr margin
55✔
101
   * @default 4
55✔
102
   */
55✔
103
  @property({ type: Number })
55✔
104
  public margin = 4;
55✔
105

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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