@jack-henry/jh-elements 2.0.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +26 -0
  3. package/README.md +13 -0
  4. package/components/badge/badge.js +73 -0
  5. package/components/button/button.js +953 -0
  6. package/components/card/card.js +343 -0
  7. package/components/checkbox/checkbox.js +601 -0
  8. package/components/checkbox-group/checkbox-group.js +241 -0
  9. package/components/divider/divider.js +91 -0
  10. package/components/icon/icon.js +96 -0
  11. package/components/input/input.js +1247 -0
  12. package/components/input-email/input-email.js +18 -0
  13. package/components/input-password/input-password.js +153 -0
  14. package/components/input-search/input-search.js +41 -0
  15. package/components/input-telephone/input-telephone.js +18 -0
  16. package/components/input-textarea/input-textarea.js +379 -0
  17. package/components/input-url/input-url.js +31 -0
  18. package/components/list-group/list-group.js +103 -0
  19. package/components/list-item/list-item.js +350 -0
  20. package/components/menu/menu.js +60 -0
  21. package/components/notification/notification.js +327 -0
  22. package/components/progress/progress.js +417 -0
  23. package/components/radio/radio.js +422 -0
  24. package/components/radio-group/radio-group.js +400 -0
  25. package/components/switch/switch.js +315 -0
  26. package/components/table/table.js +367 -0
  27. package/components/table-data-cell/table-data-cell.js +107 -0
  28. package/components/table-header-cell/table-header-cell.js +321 -0
  29. package/components/table-row/table-row.js +52 -0
  30. package/components/tag/tag.js +422 -0
  31. package/components/tag-group/tag-group.js +57 -0
  32. package/components/toast/toast.js +172 -0
  33. package/components/toast-controller/toast-controller.js +122 -0
  34. package/components/tooltip/tooltip.js +498 -0
  35. package/custom-elements.json +6892 -0
  36. package/index.d.ts +69 -0
  37. package/jsconfig.json +9 -0
  38. package/package.json +52 -0
  39. package/utils/themeProvider.js +32 -0
@@ -0,0 +1,1247 @@
1
+ // SPDX-FileCopyrightText: 2025 Jack Henry
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+
5
+ import { LitElement, css, html } from 'lit';
6
+ import { ifDefined } from 'lit/directives/if-defined.js';
7
+ import '../button/button.js';
8
+ import '@jack-henry/jh-icons/icons-wc/icon-circle-xmark.js';
9
+
10
+ let id = 0;
11
+
12
+ /**
13
+ * @cssprop --jh-input-label-color-text - The label text color. Defaults to `--jh-color-content-primary-enabled`.
14
+ * @cssprop --jh-input-field-color-background - The input field background-color. Defaults to `--jh-color-container-primary-enabled`.
15
+ * @cssprop --jh-input-field-color-border-enabled - The input field border-color. Defaults to `--jh-border-control-color`.
16
+ * @cssprop --jh-input-field-border-radius - The input field border radius. Defaults to `--jh-border-radius-100`.
17
+ * @cssprop --jh-input-color-focus - The input field outline when it receives keyboard focus. Defaults to `--jh-border-focus-color`.
18
+ * @cssprop --jh-input-field-color-border-focus - The input field border-color when in focus. Defaults to `--jh-color-content-brand-hover`.
19
+ * @cssprop --jh-input-field-color-border-hover - The input field border-color when hovered. Defaults to `--jh-color-content-brand-hover`.
20
+ * @cssprop --jh-input-field-color-border-active - The input field border-color when active. Defaults to `--jh-color-content-brand-active`.
21
+ * @cssprop --jh-input-field-color-border-disabled - The input field border-color when disabled. Defaults to `--jh-border-control-color`.
22
+ * @cssprop --jh-input-opacity-disabled - The input opacity when disabled. Defaults to `--jh-opacity-disabled`.
23
+ * @cssprop --jh-input-field-color-border-error - The input field border-color when invalid. Defaults to `--jh-border-error-color`.
24
+ * @cssprop --jh-input-clear-border-radius - The clear button border radius. Defaults to `--jh-border-radius-100`.
25
+ * @cssprop --jh-input-clear-color-background-enabled - The clear button background-color. Defaults to `transparent`.
26
+ * @cssprop --jh-input-clear-color-border-enabled - The clear button border-color. Defaults to `transparent`.
27
+ * @cssprop --jh-input-clear-icon-color-fill-enabled - The clear button icon fill color. Defaults to `--jh-color-content-brand-enabled`.
28
+ * @cssprop --jh-input-clear-color-background-focus - The clear button background-color when in focus. Defaults to `--jh-color-content-brand-hover`.
29
+ * @cssprop --jh-input-clear-color-border-focus - The clear button border-color when in focus. Defaults to `transparent`.
30
+ * @cssprop --jh-input-clear-color-focus - The clear button outline when it receives keyboard focus. Defaults to `--jh-border-focus-color`.
31
+ * @cssprop --jh-input-clear-icon-color-fill-focus - The clear button icon fill color when in focus. Defaults to `--jh-color-content-on-brand-hover`.
32
+ * @cssprop --jh-input-clear-color-background-hover - The clear button background-color when hovered. Defaults to `--jh-color-content-brand-hover`.
33
+ * @cssprop --jh-input-clear-color-border-hover - The clear button border-color when hovered. Defaults to `transparent`.
34
+ * @cssprop --jh-input-clear-icon-color-fill-hover - The clear button icon fill color when hovered. Defaults to `--jh-color-content-on-brand-hover`.
35
+ * @cssprop --jh-input-clear-color-background-active - The clear button background-color when active. Defaults to `--jh-color-content-brand-active`.
36
+ * @cssprop --jh-input-clear-color-border-active - The clear button border-color when active. Defaults to `transparent`.
37
+ * @cssprop --jh-input-clear-icon-color-fill-active - The clear button icon fill color when active. Defaults to `--jh-color-content-on-brand-active`.
38
+ * @cssprop --jh-input-required-color-text - The required indicator color. Defaults to `jh-color-content-negative-enabled`.
39
+ * @cssprop --jh-input-optional-color-text - The optional indicator text color. Defaults to `jh-color-content-primary-enabled`.
40
+ * @cssprop --jh-input-helper-color-text - The helper-text text color. Defaults to `jh-color-content-secondary-enabled`.
41
+ * @cssprop --jh-input-counter-color-text - The character counter text color. Defaults to `--jh-color-content-secondary-enabled`.
42
+ * @cssprop --jh-input-value-color-text - The value text color. Defaults to `jh-color-content-primary-enabled`.
43
+ * @cssprop --jh-input-error-color-text - The error message text color. Defaults to `jh-color-content-negative-enabled`.
44
+ *
45
+ * @event jh-select - Dispatched when text is selected. Event payload contains the selected text, the starting index of the selection, and the ending index of the selection. These values can be accessed via `e.detail.selected`, `e.detail.selectionStart`, and `e.detail.selectionEnd`.
46
+ * @event jh-change - Dispatched when the value of the input has changed and input loses focus. Event payload includes the value of the input and can be accessed via `e.detail.value`. Payload also includes the raw/unformatted value when an input mask is applied and can be accessed via `e.detail.rawValue`.
47
+ * @event jh-input - Dispatched when the value of the input has changed. Event payload includes the value of the input and can be accessed via `e.detail.value`. Payload also includes the raw/unformatted value when an input mask is applied and can be accessed via `e.detail.rawValue`.
48
+ * @event jh-maxlength - Dispatched when the `maxlength` property is set and it's value is reached.
49
+ * @event jh-input:clear-button-click - Dispatched when the clear button is activated. Event payload contains the previous value of the input field before it was cleared and can be accessed via `e.detail.previousValue`.
50
+ * @slot jh-input-left - Use to insert an element on the left side of the input field, such as an icon or button.
51
+ * @slot jh-input-right - Use to insert an element on the right side of the input field, such as an icon or button.
52
+ * @slot jh-input-clear-button - Use to insert an icon within the clear button.
53
+ *
54
+ * @customElement jh-input
55
+ */
56
+ export class JhInput extends LitElement {
57
+ static get formAssociated() {
58
+ return true;
59
+ }
60
+
61
+ /** @type {ElementInternals} */
62
+ #internals;
63
+ /** @type {?number} */
64
+ #id;
65
+ /** @type {?string} */
66
+ #value;
67
+ /** @type {string} */
68
+ #rawValue = '';
69
+ /** @type {number} */
70
+ #startLastFixedChar;
71
+ /** @type {boolean} */
72
+ #deletedChar = false;
73
+ /** @type {number} */
74
+ #adjustCaretPositionStart = null;
75
+ /** @type {Object} */
76
+ #selectedText = {
77
+ selected: false,
78
+ selectionStart: null,
79
+ selectionEnd: null,
80
+ };
81
+ /** @type {Array} */
82
+ #maskMetaCharIndexes;
83
+ /** @type {Array} */
84
+ #maskFixedCharIndexes;
85
+ /** @type {Object} */
86
+ #regexSubset = {
87
+ // digits -> phone numbers, CC, etc
88
+ '9': /[0-9]/,
89
+ // alphabetical characters -> names, address, etc
90
+ 'a': /[A-Za-z]/,
91
+ // alphanumeric characters -> usernames, product codes, etc.
92
+ '*': /[A-Za-z0-9]/,
93
+ };
94
+
95
+ static get styles() {
96
+ return css`
97
+ :host {
98
+ font-family: var(--jh-font-helper-regular-font-family);
99
+ font-weight: var(--jh-font-helper-regular-font-weight);
100
+ font-size: var(--jh-font-helper-regular-font-size);
101
+ line-height: var(--jh-font-helper-regular-line-height);
102
+ display: inline-block;
103
+ width: 100%;
104
+ --jh-button-size: var(--jh-dimension-800);
105
+ /* input padding + slot padding */
106
+ --padding-with-left-slotted-content: calc(
107
+ var(--jh-dimension-400) + var(--jh-dimension-200)
108
+ );
109
+ --padding-with-right-slotted-content: calc(
110
+ var(--jh-dimension-400) + var(--jh-dimension-200)
111
+ );
112
+ --input-value-color-text: var(
113
+ --jh-input-value-color-text,
114
+ var(--jh-color-content-primary-enabled)
115
+ );
116
+ }
117
+ label {
118
+ color: var(
119
+ --jh-input-label-color-text,
120
+ var(--jh-color-content-primary-enabled)
121
+ );
122
+ display: block;
123
+ }
124
+ .helper-text {
125
+ color: var(
126
+ --jh-input-helper-color-text,
127
+ var(--jh-color-content-secondary-enabled)
128
+ );
129
+ }
130
+ label,
131
+ .helper-text,
132
+ .error-text {
133
+ word-break: normal;
134
+ overflow-wrap: anywhere;
135
+ }
136
+ :host([show-char-count]) .helper-text {
137
+ display: inline-block;
138
+ }
139
+ .input-container {
140
+ position: relative;
141
+ }
142
+ :host([label]) .input-container {
143
+ margin-top: var(--jh-dimension-200);
144
+ }
145
+ input {
146
+ background-color: var(
147
+ --jh-input-field-color-background,
148
+ var(--jh-color-container-primary-enabled)
149
+ );
150
+ border-width: var(--jh-border-control-width);
151
+ border-style: var(--jh-border-control-style);
152
+ border-color: var(
153
+ --jh-input-field-color-border-enabled,
154
+ var(--jh-border-control-color)
155
+ );
156
+ border-radius: var(
157
+ --jh-input-field-border-radius,
158
+ var(--jh-border-radius-100)
159
+ );
160
+ color: var(--input-value-color-text);
161
+ font-family: var(--jh-font-body-regular-1-font-family);
162
+ font-weight: var(--jh-font-body-regular-1-font-weight);
163
+ font-size: var(--jh-font-body-regular-1-font-size);
164
+ line-height: var(--jh-font-body-regular-1-line-height);
165
+ padding: var(--jh-dimension-0) var(--jh-dimension-400) var(--jh-dimension-0) var(--jh-dimension-400);
166
+ box-sizing: border-box;
167
+ width: 100%;
168
+ }
169
+ .jh-input-right {
170
+ padding-right: calc(var(--padding-with-right-slotted-content) + var(--jh-input-right-width));
171
+ }
172
+ .jh-input-left {
173
+ padding-left: calc(var(--padding-with-left-slotted-content) + var(--jh-input-left-width));
174
+ }
175
+ /* slot styles */
176
+ ::slotted(*),
177
+ ::slotted(*) {
178
+ position: absolute;
179
+ display: flex;
180
+ align-items: center;
181
+ justify-content: center;
182
+ }
183
+ ::slotted([slot='jh-input-left']) {
184
+ left: var(--jh-dimension-400);
185
+ }
186
+ ::slotted([slot='jh-input-right']) {
187
+ right: var(--jh-dimension-400);
188
+ }
189
+ ::slotted([slot='jh-input-left']){
190
+ top: var(--jh-input-left-top);
191
+ }
192
+ ::slotted([slot='jh-input-right']) {
193
+ top: var(--jh-input-right-top);
194
+ }
195
+ /* clear button */
196
+ .clear-button {
197
+ right: var(--jh-dimension-400);
198
+ --jh-button-border-radius: var(--jh-input-clear-border-radius);
199
+ --jh-button-color-background-tertiary-enabled: var(--jh-input-clear-color-background-enabled);
200
+ --jh-button-color-border-tertiary-enabled: var(--jh-input-clear-color-border-enabled);
201
+ --jh-button-icon-color-fill-tertiary-enabled: var(--jh-input-clear-icon-color-fill-enabled);
202
+ --jh-button-color-background-tertiary-focus: var(--jh-input-clear-color-background-focus);
203
+ --jh-button-color-border-tertiary-focus: var(--jh-input-clear-color-border-focus);
204
+ --jh-button-color-focus: var(--jh-input-clear-color-focus);
205
+ --jh-button-icon-color-fill-tertiary-focus: var(--jh-input-clear-icon-color-fill-focus);
206
+ --jh-button-color-background-tertiary-hover: var(--jh-input-clear-color-background-hover);
207
+ --jh-button-color-border-tertiary-hover: var(--jh-input-clear-color-border-hover);
208
+ --jh-button-icon-color-fill-tertiary-hover: var(--jh-input-clear-icon-color-fill-hover);
209
+ --jh-button-color-background-tertiary-active: var(--jh-input-clear-color-background-active);
210
+ --jh-button-color-border-tertiary-active: var(--jh-input-clear-color-border-active);
211
+ --jh-button-icon-color-fill-tertiary-active: var(--jh-input-clear-icon-color-fill-active);
212
+ display: none;
213
+ position: absolute;
214
+ }
215
+ .display-clear-button .clear-button {
216
+ display: inherit;
217
+ }
218
+ :host([size='small']) .clear-button {
219
+ top: 4px;
220
+ }
221
+ :host([size='medium']) .clear-button {
222
+ top: 8px;
223
+ }
224
+ :host([size='large']) .clear-button {
225
+ top: 12px;
226
+ }
227
+ .jh-input-right ~ .clear-button {
228
+ right: calc(var(--padding-with-right-slotted-content) + var(--jh-input-right-width));
229
+ }
230
+ .display-clear-button input {
231
+ padding-right: calc(var(--padding-with-right-slotted-content) + var(--jh-dimension-800));
232
+ }
233
+ .display-clear-button .jh-input-right {
234
+ padding-right: calc(var(--padding-with-right-slotted-content) + var(--jh-input-right-width) + var(--jh-dimension-800) + var(--jh-dimension-200));
235
+ }
236
+ /* Sizes */
237
+ :host([size='small']) input {
238
+ height: var(--jh-dimension-1000);
239
+ }
240
+ :host([size='medium']) input {
241
+ height: var(--jh-dimension-1200);
242
+ }
243
+ :host([size='large']) input {
244
+ height: var(--jh-dimension-1400);
245
+ }
246
+ .footer-content {
247
+ margin: var(--jh-dimension-200) 0 0 0;
248
+ gap: var(--jh-dimension-200);
249
+ display: flex;
250
+ justify-content: space-between;
251
+ }
252
+ .footer-content:has(.counter):not(:has(.error-text)) {
253
+ justify-content: flex-end;
254
+ }
255
+ .counter {
256
+ color: var(--jh-input-counter-color-text,
257
+ var(--jh-color-content-secondary-enabled)
258
+ );
259
+ }
260
+ .error-text {
261
+ color: var(
262
+ --jh-input-error-color-text,
263
+ var(--jh-color-content-negative-enabled)
264
+ );
265
+ }
266
+ p {
267
+ margin: 0;
268
+ }
269
+ /* Input States */
270
+ input:active {
271
+ border-color: var(
272
+ --jh-input-field-color-border-active,
273
+ var(--jh-color-content-brand-active)
274
+ );
275
+ }
276
+ :host([disabled]) {
277
+ opacity: var(--jh-input-opacity-disabled, var(--jh-opacity-disabled));
278
+ }
279
+ :host([disabled]) input {
280
+ border-color: var(
281
+ --jh-input-field-color-border-disabled,
282
+ var(--jh-border-control-color)
283
+ );
284
+ }
285
+ input:focus-visible {
286
+ border-color: var(
287
+ --jh-input-field-color-border-focus,
288
+ var(--jh-color-content-brand-hover)
289
+ );
290
+ outline-color: var(
291
+ --jh-input-color-focus,
292
+ var(--jh-border-focus-color)
293
+ );
294
+ outline-style: var(--jh-border-focus-style);
295
+ outline-width: var(--jh-border-focus-width);
296
+ outline-offset: 1px;
297
+ }
298
+ input:hover {
299
+ border-color: var(
300
+ --jh-input-field-color-border-hover,
301
+ var(--jh-color-content-brand-hover)
302
+ );
303
+ }
304
+ :host([invalid]) input {
305
+ border-width: var(--jh-border-error-width);
306
+ border-style: var(--jh-border-error-style);
307
+ border-color: var(
308
+ --jh-input-field-color-border-error,
309
+ var(--jh-border-error-color)
310
+ );
311
+ }
312
+ /* readonly styles */
313
+ :host([readonly]) input {
314
+ height: auto;
315
+ background-color: transparent;
316
+ border: none;
317
+ padding-left: 0;
318
+ padding-right: 0;
319
+ }
320
+ /* Override Chrome autofill styles */
321
+ input:autofill {
322
+ -webkit-text-fill-color: var(--input-value-color-text);
323
+ caret-color: var(--input-value-color-text);
324
+ background-clip: text;
325
+ }
326
+ /* Optional/Required/Show-indicator */
327
+ :host([show-indicator]) span {
328
+ color: var(
329
+ --jh-input-optional-color-text,
330
+ var(--jh-color-content-primary-enabled)
331
+ );
332
+ }
333
+ :host([show-indicator][required]) span {
334
+ color: var(
335
+ --jh-input-required-color-text,
336
+ var(--jh-color-content-negative-enabled)
337
+ );
338
+ }
339
+
340
+ `;
341
+ }
342
+
343
+ static get properties() {
344
+ return {
345
+ /** Sets an `aria-label` on the input field to assist screen reader users when no visible label is present. */
346
+ accessibleLabel: { type: String, attribute: 'accessible-label' },
347
+ /** Sets an aria-label on the clear button to assist screen reader users. Indicates that activating the button will clear the input field. */
348
+ accessibleLabelClearButton: { type: String, attribute: 'accessible-label-clear-button'},
349
+ /**
350
+ * Determines whether the browser can provide assistance in filling out the input value and what type of information is expected.
351
+ * This property will override any autocomplete attribute present on the input's parent form element.
352
+ *
353
+ * [Visit MDN for information on supported autocomplete values](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete)
354
+ */
355
+ autocomplete: { type: String },
356
+ /** Disables the input and prevents all user interactions. May cause the input to be ignored by assistive technologies (AT). */
357
+ disabled: { type: Boolean },
358
+ /** Specifies which action label or icon to present for the enter key on virtual keyboards.
359
+ *
360
+ * [Visit MDN for information on supported enterkeyhint values](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/enterkeyhint)
361
+ */
362
+ enterkeyhint: { type: String },
363
+ /** Text to be displayed when input has failed validation and `invalid` is true. */
364
+ errorText: { type: String, attribute: 'error-text' },
365
+ /** Provides additional context or guidance for using the input. For `helper-text` to be displayed, the `label` property must also be set. */
366
+ helperText: { type: String, attribute: 'helper-text' },
367
+ /** Hides the left slot from input. */
368
+ hideLeftSlot: { type: Boolean, attribute: 'hide-left-slot' },
369
+ /** Hides the right slot from input. */
370
+ hideRightSlot: { type: Boolean, attribute: 'hide-right-slot' },
371
+ /** Formats the user entered data on input. Does not apply to pasted values. See input mask documentation above for implementation details. */
372
+ inputMask: { type: String, attribute: 'input-mask' },
373
+ /** Indicates expected input value type and allows for browsers to display appropriate virtual keyboard.
374
+ *
375
+ * [Visit MDN for information on supported inputmode values](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/inputmode)
376
+ */
377
+ inputmode: { type: String },
378
+ /** Sets an `aria-invalid` attribute on input to indicate the value supplied was invalid. Also displays `error-text` and error state styling when set. */
379
+ invalid: { type: Boolean },
380
+ /** Identifies what data should be entered into the input field. */
381
+ label: { type: String },
382
+ /** Sets the maximum number of characters a user can enter into the field. */
383
+ maxlength: { type: String },
384
+ /** Sets the minimum number of characters a user can enter into the field. */
385
+ minlength: { type: String },
386
+ /** Sets a name for the input control. */
387
+ name: { type: String },
388
+ /** Prevents users from changing the input value. Removes all slotted content. */
389
+ readonly: { type: Boolean },
390
+ /** Indicates a value is required. */
391
+ required: { type: Boolean },
392
+ /** Displays a character counter at the bottom right corner below the input field. */
393
+ showCharCount: { type: Boolean, attribute: 'show-char-count' },
394
+ /** Displays a clear button in the input field when it contains a value and is focused or hovered. Deletes input value when activated. */
395
+ showClearButton: {type: Boolean, attribute: 'show-clear-button'},
396
+ /** Adds a visual indicator next to the label. Indicates that a value is optional(by default) or required if the `required` property is also set. */
397
+ showIndicator: { type: Boolean, attribute: 'show-indicator' },
398
+ /** Sets the size of the input. */
399
+ size: { type: String, reflect: true },
400
+ /** Sets the value of the input. */
401
+ value: { type: String },
402
+ };
403
+ }
404
+
405
+ constructor() {
406
+ super();
407
+ this.#internals = this.attachInternals();
408
+ /** @type {?string} */
409
+ this.accessibleLabel = null;
410
+ /** @type {?string} */
411
+ this.accessibleLabelClearButton = null;
412
+ /** @type {?string} */
413
+ this.autocomplete = null;
414
+ /** @type {boolean} */
415
+ this.disabled = false;
416
+ /** @type {?string} */
417
+ this.enterkeyhint = null;
418
+ /** @type {?string} */
419
+ this.errorText = null;
420
+ /** @type {?string} */
421
+ this.helperText = null;
422
+ /** @type {boolean} */
423
+ this.hideLeftSlot = false;
424
+ /** @type {boolean} */
425
+ this.hideRightSlot = false;
426
+ /** @type {?string} */
427
+ this.inputMask = null;
428
+ /** @type {?string} */
429
+ this.inputmode = null;
430
+ /** @type {boolean} */
431
+ this.invalid = false;
432
+ /** @type {?string} */
433
+ this.label = null;
434
+ /** @type {?string} */
435
+ this.maxlength = null;
436
+ /** @type {?string} */
437
+ this.minlength = null;
438
+ /** @type {?string} */
439
+ this.name = null;
440
+ /** @type {boolean} */
441
+ this.readonly = false;
442
+ /** @type {boolean} */
443
+ this.required = false;
444
+ /** @type {boolean} */
445
+ this.showCharCount = false;
446
+ /** @type {boolean} */
447
+ this.showClearButton = false;
448
+ /** @type {boolean} */
449
+ this.showIndicator = false;
450
+ /** @type {'small'|'medium'|'large'} */
451
+ this.size = 'medium';
452
+ /** @type {?string} */
453
+ this.value = null;
454
+ }
455
+
456
+ connectedCallback() {
457
+ super.connectedCallback();
458
+ this.#id = id++;
459
+ this.#captureMaskIndexes();
460
+ let observer = new MutationObserver(this.#captureMaskIndexes.bind(this));
461
+ observer.observe(this, { attributeFilter: ['input-mask'] });
462
+ this.addEventListener('jh-select', this.#setSelection);
463
+ }
464
+
465
+ disconnectedCallback() {
466
+ super.disconnectedCallback();
467
+ if (this.inputMask) {
468
+ this.removeEventListener('jh-select', this.#setSelection);
469
+ }
470
+ }
471
+
472
+ firstUpdated() {
473
+ // attach event listeners to show/hide clear button
474
+ if (this.showClearButton) {
475
+ ['focusin', 'focusout'].forEach(e => {
476
+ this.addEventListener(e, this.#toggleFocus);
477
+ });
478
+ let inputContainer = this.shadowRoot.querySelector('.input-container');
479
+ ['mouseenter', 'mouseleave'].forEach(e => {
480
+ inputContainer.addEventListener(e, this.#toggleFocus.bind(this));
481
+ });
482
+ }
483
+ }
484
+
485
+ #toggleFocus(e) {
486
+ if (this.disabled || this.readonly || !this.showClearButton) {
487
+ return;
488
+ }
489
+
490
+ // clear button to remain visible on mouseleave if input field or slotted content is focused
491
+ if (this.matches(':focus-within') || e.type === 'mouseenter') {
492
+ this.shadowRoot.querySelector('.input-container').classList.add('display-clear-button');
493
+ } else {
494
+ this.shadowRoot.querySelector('.input-container').classList.remove('display-clear-button');
495
+ }
496
+ }
497
+
498
+ // create index of meta and fixed chars of input mask w/ corresponding formatted value indexes
499
+ #captureMaskIndexes() {
500
+
501
+ if (!this.inputMask) {
502
+ return;
503
+ }
504
+
505
+ let maskIndex = 0;
506
+ let escapeCharCount = 0;
507
+ let fixedCharCount = 0;
508
+ let mask = this.inputMask;
509
+ let lastCharIsFixed = false;
510
+ this.#maskFixedCharIndexes = [];
511
+ this.#maskMetaCharIndexes = [];
512
+
513
+ while (maskIndex < mask.length) {
514
+ let maskChar = mask[maskIndex];
515
+ let metaChar = this.#regexSubset[maskChar];
516
+
517
+ // escape char located, record fixed char index and skip to next char
518
+ if (maskChar === '\\' && mask[maskIndex + 1] === '\\') {
519
+ escapeCharCount += 2;
520
+ this.#maskFixedCharIndexes.push({'maskIndex' : maskIndex + 2, 'escapeCharCount': escapeCharCount, 'fixedCharCount': fixedCharCount, 'formattedValIndex': maskIndex + 2 - escapeCharCount});
521
+ maskIndex += 3;
522
+ fixedCharCount++;
523
+ continue;
524
+ }
525
+
526
+ if (!metaChar) {
527
+ lastCharIsFixed = maskIndex === mask.length - 1;
528
+ this.#maskFixedCharIndexes.push({'maskIndex' : maskIndex, 'escapeCharCount': escapeCharCount, 'fixedCharCount': fixedCharCount, 'formattedValIndex': maskIndex - escapeCharCount});
529
+ maskIndex++;
530
+ fixedCharCount++;
531
+ } else {
532
+ this.#maskMetaCharIndexes.push({'maskIndex' : maskIndex, 'escapeCharCount': escapeCharCount, 'fixedCharCount': fixedCharCount, 'formattedValIndex': maskIndex - escapeCharCount });
533
+ maskIndex++;
534
+ }
535
+ }
536
+
537
+ if (lastCharIsFixed) {
538
+ this.#startLastFixedChar = this.#captureLastFixedCharIndex()
539
+ } else {
540
+ this.#startLastFixedChar = -1;
541
+ }
542
+ }
543
+
544
+ #setSelection(e) {
545
+ this.#selectedText = {
546
+ selected: true,
547
+ selectionStart: e.detail.selectionStart,
548
+ selectionEnd: e.detail.selectionEnd,
549
+ }
550
+ }
551
+
552
+ /** @ignore */
553
+ get form() {
554
+ return this.#internals.form;
555
+ }
556
+
557
+ get value() {
558
+ return this.#value;
559
+ }
560
+
561
+ set value(newValue) {
562
+ const oldValue = this.#value;
563
+ if (newValue !== oldValue) {
564
+ this.#value = newValue;
565
+ this.#internals.setFormValue(this.#value);
566
+ }
567
+ this.requestUpdate('value', oldValue);
568
+ }
569
+
570
+ #dispatch(eventName, details) {
571
+ this.dispatchEvent(
572
+ new CustomEvent(eventName, {
573
+ detail: details,
574
+ bubbles: true,
575
+ cancelable: true,
576
+ composed: true,
577
+ })
578
+ );
579
+ }
580
+
581
+ #handleInput(e) {
582
+ this.value = e.target.value;
583
+ let inputType = e.inputType;
584
+ this.#deletedChar = inputType === 'deleteContentBackward' || inputType === 'deleteByCut' || inputType === 'deleteContentForward';
585
+
586
+ if (this.inputMask) {
587
+ // format only typed values; no paste
588
+ if (inputType === 'insertText' || this.#deletedChar) {
589
+ this.#applyInputMask(e);
590
+ }
591
+ } else {
592
+ this.#dispatch('jh-input', { value: this.value });
593
+ }
594
+ }
595
+
596
+ #handleKeydown(e) {
597
+ const value = e.target.value;
598
+ let selectionStart = e.target.selectionStart;
599
+ let selectionEnd = e.target.selectionEnd;
600
+
601
+
602
+ const testKey = (metaChar, key) => {
603
+ if (!this.#regexSubset[metaChar].test(key)) {
604
+ e.preventDefault();
605
+ }
606
+ }
607
+
608
+ // don't validate autofill or keyboard shortcuts ie ctrl + c, ctrl + v, etc
609
+ if (e.key === undefined || e.ctrlKey || e.metaKey) {
610
+ return;
611
+ }
612
+
613
+ // only validate single char keys
614
+ if (e.key.length === 1) {
615
+ if (selectionStart < value.length) {
616
+ this.#validateInsertion(e, selectionStart, testKey);
617
+ } else {
618
+ this.#validateAppending(e, testKey);
619
+ }
620
+ }
621
+
622
+ if (e.key === 'Backspace' || e.key === 'Delete') {
623
+ this.#checkForFixedCharRemoval(e, selectionStart, selectionEnd);
624
+ }
625
+ }
626
+
627
+ #checkForFixedCharRemoval(e, selectionStart, selectionEnd) {
628
+
629
+ if (e.key === 'Backspace') {
630
+ if (this.#selectedText?.selected) {
631
+ let fixedCharSelected = this.#maskFixedCharIndexes.find((fixedChar) => fixedChar.formattedValIndex === selectionStart);
632
+ let metaCharSelected = this.#maskMetaCharIndexes.find((metaChar) => metaChar.formattedValIndex >= selectionStart && metaChar.formattedValIndex < selectionEnd);
633
+
634
+ // if user only selected fixed char(s), move caret to beginning of selection
635
+ if (fixedCharSelected && !metaCharSelected) {
636
+ this.#setCaretPosition(e.target, 'Backspace', selectionStart);
637
+ this.#selectedText.selected = false;
638
+ e.preventDefault();
639
+ }
640
+ } else {
641
+ // if user deleted a fixed char, move caret before fixed char
642
+ let fixedCharDeleted = this.#maskFixedCharIndexes.find((fixedChar) => fixedChar.formattedValIndex === selectionStart - 1);
643
+
644
+ if (fixedCharDeleted) {
645
+ this.#setCaretPosition(e.target, 'Backspace', selectionStart - 1);
646
+ }
647
+ }
648
+ }
649
+
650
+ if (e.key === 'Delete') {
651
+ if (this.#selectedText?.selected) {
652
+ let fixedCharSelected = this.#maskFixedCharIndexes.find((fixedChar) => fixedChar.formattedValIndex === selectionStart);
653
+ let metaCharSelected = this.#maskMetaCharIndexes.find((metaChar) => metaChar.formattedValIndex >= selectionStart && metaChar.formattedValIndex < selectionEnd);
654
+
655
+ // if user only selected fixed char(s), move caret to end of selection
656
+ if (fixedCharSelected && !metaCharSelected) {
657
+ this.#setCaretPosition(e.target, 'Delete', selectionStart, selectionEnd);
658
+ this.#selectedText.selected = false;
659
+ e.preventDefault();
660
+ }
661
+ } else {
662
+ // if user deleted a fixed char, move caret behind fixed char
663
+ let fixedCharDeleted = this.#maskFixedCharIndexes.find((fixedChar) => fixedChar.formattedValIndex === selectionStart);
664
+
665
+ if (fixedCharDeleted) {
666
+ this.#setCaretPosition(e.target, 'Delete', selectionStart);
667
+ }
668
+ }
669
+ }
670
+ }
671
+
672
+ // move caret to prevent deletion of fixed chars
673
+ #setCaretPosition(input, key, selectionStart, selectionEnd) {
674
+ let metaCharIndex;
675
+ let newCaretIndex;
676
+
677
+ // find the first meta char before the fixed char that user attempted to delete
678
+ if (key === "Backspace") {
679
+
680
+ if (this.#selectedText?.selected) {
681
+ // user only selected fixed char(s), move caret to beginning of selection
682
+ newCaretIndex = selectionStart;
683
+ } else {
684
+ // find last meta char before fixed char that user attempted to delete
685
+ metaCharIndex = this.#maskMetaCharIndexes.toReversed().find((metaChar) => metaChar.formattedValIndex <= selectionStart);
686
+
687
+ if (metaCharIndex) {
688
+ // place caret after last meta char before fixed char so it can be deleted
689
+ newCaretIndex = metaCharIndex.formattedValIndex + 1;
690
+ } else {
691
+ // place caret at beginning of input if user attempting to delete fixed char at beginning of input mask
692
+ newCaretIndex = 0;
693
+ }
694
+ }
695
+ }
696
+
697
+ // find the first meta char after the fixed char that user attempted to delete
698
+ if (key === "Delete") {
699
+
700
+ if (this.#selectedText?.selected) {
701
+ // user only selected fixed char(s), move caret to end of selection
702
+ newCaretIndex = selectionEnd;
703
+ } else {
704
+ // find first meta char after fixed char that user attempted to delete
705
+ metaCharIndex = this.#maskMetaCharIndexes.find((metaChar) => metaChar.formattedValIndex >= selectionStart);
706
+
707
+ if (metaCharIndex) {
708
+ // place caret before first meta char after fixed char so it can be deleted
709
+ newCaretIndex = metaCharIndex.formattedValIndex;
710
+ } else {
711
+ // place caret at end of input if no following meta char
712
+ if (this.#startLastFixedChar >= 0) {
713
+ newCaretIndex = this.#maskFixedCharIndexes[this.#maskFixedCharIndexes.length - 1].formattedValIndex + 1;
714
+ }
715
+ }
716
+ }
717
+ }
718
+ input.setSelectionRange(newCaretIndex, newCaretIndex);
719
+ }
720
+
721
+ #validateInsertion(e, selectionStart, testKey) {
722
+ // Find corresponding mask and fixed char indexes
723
+ let metaCharIndex = this.#maskMetaCharIndexes.find((metaChar) => metaChar.formattedValIndex === selectionStart);
724
+ let fixedCharIndex = this.#maskFixedCharIndexes.find((fixedChar) => fixedChar.formattedValIndex === selectionStart);
725
+
726
+ // inserting char at index of a fixed char; find following meta char
727
+ if (fixedCharIndex) {
728
+ // set meta char index to first meta char following fixed char
729
+ metaCharIndex = this.#maskMetaCharIndexes.find((metaChar) => metaChar.formattedValIndex >= fixedCharIndex.formattedValIndex + 1);
730
+ }
731
+
732
+ // test key against meta char
733
+ if (metaCharIndex) {
734
+ testKey(this.inputMask[metaCharIndex.maskIndex], e.key);
735
+ }
736
+ }
737
+
738
+ #validateAppending(e, testKey) {
739
+ let metaCharIndex = this.#maskMetaCharIndexes[this.#rawValue.length]?.maskIndex;
740
+ if (metaCharIndex !== undefined) {
741
+ testKey(this.inputMask[metaCharIndex], e.key);
742
+ }
743
+ }
744
+
745
+ #captureLastFixedCharIndex() {
746
+ // initialize index to the last element in the mask fixed character indexes array
747
+ let index = this.#maskFixedCharIndexes.length - 1;
748
+
749
+ for (let i = this.#maskFixedCharIndexes.length - 1; i >= 0; i--) {
750
+ let previousIndex = i - 1;
751
+
752
+ // check if the current fixed char follows immediately after fixed char, ie input-mask="99AA"
753
+ if (this.#maskFixedCharIndexes[i].maskIndex === this.#maskFixedCharIndexes[previousIndex]?.maskIndex + 1) {
754
+ index = previousIndex;
755
+ } else {
756
+ // break the loop if the fixed chars are not consecutive
757
+ break;
758
+ }
759
+ }
760
+ return index;
761
+ }
762
+
763
+ #removeMask(e, value) {
764
+ let insertedChar = e.target.selectionStart < value.length;
765
+ this.#adjustCaretPositionStart = insertedChar ? e.target.selectionStart : null;
766
+ let replacedChar = this.#selectedText?.selected;
767
+ let valueArray = value.split('');
768
+
769
+ if (replacedChar) {
770
+ // handle deletion or replacement of selected text
771
+ return this.#handleReplacement(valueArray);
772
+ }
773
+ if (this.#deletedChar) {
774
+ // handle deletion of single char
775
+ return this.#handleDeletion(e, valueArray);
776
+ }
777
+ if (insertedChar) {
778
+ // handle insertion of single char
779
+ return this.#handleInsertion(e, valueArray);
780
+ }
781
+ // handle appending of single char
782
+ return this.#handleAppending(valueArray);
783
+ }
784
+
785
+ #handleReplacement(valueArray) {
786
+ let selectionStart = this.#selectedText.selectionStart;
787
+ let selectionEnd = this.#selectedText.selectionEnd;
788
+
789
+ // browsers delete blank space before selection start on backspace when user double clicks to select text to be deleted
790
+ if (this.#deletedChar) {
791
+ this.#reinsertBlankSpace(selectionStart, selectionEnd, valueArray);
792
+ }
793
+
794
+ this.#maskFixedCharIndexes.toReversed().forEach((fixedChar) => {
795
+
796
+ let selectedFixedChars = fixedChar.formattedValIndex >= selectionStart && fixedChar.formattedValIndex < selectionEnd;
797
+
798
+ // fixed char within selection and deleted, no longer present in valueArray
799
+ if (selectedFixedChars) {
800
+ return;
801
+ }
802
+
803
+ // fixed char after selection, adjust index of fixed char to account for deleted/replaced chars
804
+ if (fixedChar.formattedValIndex >= selectionEnd) {
805
+ if (this.#deletedChar) {
806
+ // get new index of fixed char
807
+ let adjustedIndex = fixedChar.formattedValIndex - (selectionEnd - selectionStart);
808
+ return valueArray.splice(adjustedIndex, 1);
809
+ } else {
810
+ // get new index of fixed char, subtract 1 to account for added char within selection
811
+ let adjustedIndex = fixedChar.formattedValIndex - (selectionEnd - selectionStart - 1);
812
+ return valueArray.splice(adjustedIndex, 1);
813
+ }
814
+ }
815
+ // fixed char before selection, no adjustment needed
816
+ valueArray.splice(fixedChar.formattedValIndex, 1);
817
+ });
818
+ this.#selectedText.selected = false;
819
+ return valueArray.join('');
820
+ }
821
+
822
+ #reinsertBlankSpace(selectionStart, selectionEnd, valueArray) {
823
+ let previousFixedChar = this.#maskFixedCharIndexes.find((fixedChar) => fixedChar.formattedValIndex === selectionStart - 1);
824
+
825
+ if (previousFixedChar && this.inputMask[previousFixedChar.maskIndex] === ' ') {
826
+ //check to see if following fixed char is also a blank space
827
+ let followingFixedChar = this.#maskFixedCharIndexes.find((fixedChar) => fixedChar.formattedValIndex === selectionEnd);
828
+
829
+ if (this.inputMask[followingFixedChar.maskIndex] === ' ') {
830
+ // if blank space located at beginning and end of selection, there should be two blank spaces in valueArray
831
+ if (valueArray[selectionStart - 1] !== ' ' || valueArray[selectionStart] !== ' ') {
832
+ valueArray.splice(selectionStart - 1, 0, ' ');
833
+ }
834
+ } else {
835
+ // blank space may be present if user clicked and dragged to select text
836
+ if (valueArray[selectionStart - 1] !== ' ') {
837
+ valueArray.splice(selectionStart - 1, 0, ' ');
838
+ }
839
+ }
840
+ }
841
+ return valueArray;
842
+ }
843
+
844
+ #handleDeletion(e, valueArray) {
845
+ let selectionStart = e.target.selectionStart;
846
+
847
+ this.#maskFixedCharIndexes.toReversed().forEach((fixedChar) => {
848
+
849
+ // fixed char deleted, no longer present in valueArray
850
+ if (selectionStart === fixedChar.formattedValIndex) {
851
+ return;
852
+ }
853
+
854
+ // deleted char before fixed char, decrease index to account for deleted char
855
+ if (selectionStart < fixedChar.formattedValIndex) {
856
+ return valueArray.splice(fixedChar.formattedValIndex - 1, 1);
857
+ }
858
+
859
+ valueArray.splice(fixedChar.formattedValIndex, 1);
860
+ });
861
+ return valueArray.join('');
862
+ }
863
+
864
+ #handleInsertion(e, valueArray) {
865
+ let selectionStart = e.target.selectionStart;
866
+
867
+ this.#maskFixedCharIndexes.toReversed().forEach((fixedChar) => {
868
+ let fixedCharIndex = fixedChar.formattedValIndex;
869
+
870
+ // inserted char at or before fixed char, increase index to account for new char
871
+ if (selectionStart - 1 === fixedCharIndex || selectionStart <= fixedCharIndex) {
872
+ valueArray.splice(fixedChar.formattedValIndex + 1, 1);
873
+ } else {
874
+ // inserted char after fixed char
875
+ valueArray.splice(fixedChar.formattedValIndex, 1);
876
+ }
877
+ });
878
+ return valueArray.join('');
879
+ }
880
+
881
+ #handleAppending(valueArray) {
882
+ this.#maskFixedCharIndexes.toReversed().forEach((fixedChar) => {
883
+ // remove fixed chars that are located in the value
884
+ if (valueArray.length - 1 > fixedChar.formattedValIndex) {
885
+ valueArray.splice(fixedChar.formattedValIndex, 1);
886
+ }
887
+ });
888
+ return valueArray.join('');
889
+ }
890
+
891
+ #applyInputMask(e) {
892
+ // only remove mask if fixed chars are present
893
+ this.#rawValue = this.#maskFixedCharIndexes.length ? this.#removeMask(e, this.value) : this.value;
894
+ const mask = this.inputMask;
895
+
896
+ // create array to store formatted result, initialized with null values
897
+ let formattedResult = this.#initializeFormattedResult();
898
+ const initialResultLength = formattedResult.length;
899
+
900
+ // add fixed and meta chars to result array
901
+ this.#populateFormattedResult(mask, formattedResult);
902
+
903
+ // remove null values from result array
904
+ formattedResult = formattedResult.filter(value => value !== null);
905
+
906
+ // if last char of mask is fixed, add it to the formatted result when index is reached
907
+ if (this.#startLastFixedChar >= 0) {
908
+ // get index of last fixed char in the input mask
909
+ let indexOfLastFixedChar = this.#maskFixedCharIndexes[this.#startLastFixedChar]?.formattedValIndex;
910
+
911
+ // if following char is the ending fixed char of the input mask, add it to the formatted result. Account for temp loss of fixed char when deleted
912
+ if (formattedResult.length === indexOfLastFixedChar) {
913
+ this.#appendEndingFixedChars(mask, formattedResult);
914
+ }
915
+ }
916
+
917
+ // if input mask length is reached, add additional chars to the formatted result
918
+ if (formattedResult.length === initialResultLength) {
919
+ let additionalCharacters = this.#getAdditionalCharacters(formattedResult, mask);
920
+
921
+ if (additionalCharacters) {
922
+ formattedResult.push(additionalCharacters);
923
+ }
924
+ }
925
+
926
+ // remove chars that exceed maxlength
927
+ if (this.maxlength && formattedResult.length > Number(this.maxlength)) {
928
+ formattedResult = formattedResult.slice(0, this.maxlength);
929
+ }
930
+
931
+ this.value = formattedResult.join('');
932
+
933
+ // Dispatch a custom event with the formatted and raw values
934
+ this.#dispatch('jh-input', {
935
+ 'value': this.value,
936
+ 'rawValue': this.#rawValue
937
+ });
938
+ }
939
+
940
+ #initializeFormattedResult() {
941
+ return Array(Math.max(...this.#maskFixedCharIndexes.map(fixedChar => fixedChar.formattedValIndex).concat(this.#maskMetaCharIndexes.map(metaChar => metaChar.formattedValIndex))) + 1).fill(null);
942
+ }
943
+
944
+ #populateFormattedResult(mask, formattedResult) {
945
+ // reinsert fixed chars into formatted result
946
+ this.#maskFixedCharIndexes.forEach((fixedChar) => {
947
+
948
+ // add fixed chars within the length of the total value (raw value + fixed chars)
949
+ if (fixedChar.formattedValIndex < fixedChar.fixedCharCount + this.#rawValue.length) {
950
+ formattedResult[fixedChar.formattedValIndex] = mask[fixedChar.maskIndex];
951
+ }
952
+ });
953
+
954
+ // reinsert meta chars into formatted result
955
+ this.#maskMetaCharIndexes.forEach(metaChar => {
956
+ // add chars that are within the length of the total value (raw value + fixed chars)
957
+ if (metaChar.formattedValIndex < metaChar.fixedCharCount + this.#rawValue.length) {
958
+ formattedResult[metaChar.formattedValIndex] = this.#rawValue[metaChar.formattedValIndex - metaChar.fixedCharCount];
959
+ }
960
+ });
961
+ }
962
+
963
+ #appendEndingFixedChars(mask, formattedResult) {
964
+ let lastFixedCharMaskIndex = this.#maskFixedCharIndexes[this.#startLastFixedChar].maskIndex;
965
+
966
+ let endMaskIndex = mask.length;
967
+
968
+ // capture fixed characters from the input mask
969
+ let endingFixedChars = mask.slice(
970
+ lastFixedCharMaskIndex,
971
+ endMaskIndex
972
+ );
973
+
974
+ endingFixedChars.split('').forEach((char) => {
975
+ formattedResult.push(char);
976
+ });
977
+ }
978
+
979
+ #getAdditionalCharacters(formattedResult, mask) {
980
+ if (this.#maskFixedCharIndexes.length) {
981
+ // account for fixed chars present in the value
982
+ return this.#rawValue.slice(formattedResult.length - this.#maskFixedCharIndexes.length);
983
+ }
984
+ return this.#rawValue.slice(mask.length);
985
+ }
986
+
987
+ // restore caret position after input mask is applied if change to the value is within the value length
988
+ updated(changedProperties) {
989
+ if (this.#adjustCaretPositionStart) {
990
+ if (changedProperties.has('value')) {
991
+ let input = this.shadowRoot.querySelector('input');
992
+ let selectionStart = this.#selectedText.selectionStart ? this.#selectedText.selectionStart : this.#adjustCaretPositionStart;
993
+
994
+ input.setSelectionRange(selectionStart, selectionStart);
995
+ this.#selectedText.selectionStart = null;
996
+ }
997
+ }
998
+ }
999
+
1000
+ #handleChange() {
1001
+ let payload = {
1002
+ 'value': this.value,
1003
+ }
1004
+
1005
+ if (this.inputMask) {
1006
+ payload.rawValue = this.#rawValue;
1007
+ }
1008
+
1009
+ this.#dispatch('jh-change', payload);
1010
+ }
1011
+
1012
+ #handleSelect(e) {
1013
+ const selectedString = e.target.value.substring(
1014
+ e.target.selectionStart,
1015
+ e.target.selectionEnd
1016
+ );
1017
+
1018
+ // ensure selected string present before dispatching event. Can be empty due to caret positioning when user attempts to delete fixed char.
1019
+ if (selectedString) {
1020
+ this.#dispatch('jh-select', {
1021
+ selected: selectedString,
1022
+ selectionStart: e.target.selectionStart,
1023
+ selectionEnd: e.target.selectionEnd
1024
+ });
1025
+ }
1026
+ }
1027
+
1028
+ #handleMaxlength() {
1029
+ this.#dispatch('jh-maxlength');
1030
+ }
1031
+
1032
+ #handleClearButtonClick() {
1033
+ let previousValue = this.value;
1034
+ // clear input value
1035
+ this.value = '';
1036
+ // focus input field
1037
+ this.shadowRoot.querySelector('input').focus();
1038
+ // dispatch clear event
1039
+ this.dispatchEvent(
1040
+ new CustomEvent('jh-input:clear-button-click', {
1041
+ detail: {
1042
+ 'previousValue': previousValue
1043
+ },
1044
+ bubbles: true,
1045
+ cancelable: true,
1046
+ composed: true,
1047
+ })
1048
+ );
1049
+ }
1050
+
1051
+ #handleSlotChange(e) {
1052
+ let inputEl = this.shadowRoot.querySelector('input');
1053
+ let slottedElement = e.target.assignedElements()[0];
1054
+ let slotName = e.target.name;
1055
+
1056
+ if (slottedElement?.tagName.startsWith('JH-ICON')) {
1057
+ slottedElement.setAttribute('size', 'medium');
1058
+ }
1059
+
1060
+ // capture dimensions of slotted content
1061
+ if (slottedElement) {
1062
+ if (slotName === 'jh-input-left' || slotName === 'jh-input-right') {
1063
+
1064
+ // set a CSS variable for the width of the slotted content
1065
+ this.style.setProperty(`--${slottedElement.slot}-width`, `${slottedElement.offsetWidth}px`);
1066
+
1067
+ // set a css variable to vertically center slotted content
1068
+ this.style.setProperty(`--${slottedElement.slot}-top`, `${(inputEl.offsetHeight - slottedElement.offsetHeight) / 2}px`);
1069
+ }
1070
+ }
1071
+ this.#addClass(slotName, slottedElement);
1072
+ }
1073
+
1074
+ // sets class on input element so padding can accomodate slotted content
1075
+ #addClass(slotName, slottedElement) {
1076
+ const inputEl = this.shadowRoot.querySelector('input');
1077
+
1078
+ // add and remove class if slotted element is not present
1079
+ if (!slottedElement) {
1080
+ inputEl.classList.remove(slotName);
1081
+ } else {
1082
+ inputEl.classList.add(slotName);
1083
+ }
1084
+ }
1085
+
1086
+ #getSlots() {
1087
+ if (this.readonly) {
1088
+ return;
1089
+ }
1090
+
1091
+ const leftSlot = this.hideLeftSlot
1092
+ ? null
1093
+ : html`<slot
1094
+ name="jh-input-left"
1095
+ @slotchange=${this.#handleSlotChange}
1096
+ ></slot>
1097
+ `;
1098
+
1099
+ const clearBtn = this.showClearButton && this.value && !this.disabled
1100
+ ? html`
1101
+ <jh-button
1102
+ size="small" appearance="tertiary" class="clear-button"
1103
+ accessible-label=${ifDefined(this.accessibleLabelClearButton)}
1104
+ @click=${this.#handleClearButtonClick}>
1105
+ <slot name="jh-input-clear-button" slot="jh-button-icon">
1106
+ <jh-icon-circle-xmark slot="jh-button-icon" aria-hidden="true" size="medium"></jh-icon-circle-xmark>
1107
+ </slot>
1108
+ </jh-button>
1109
+ `
1110
+ : null;
1111
+
1112
+
1113
+ const rightSlot = this.hideRightSlot
1114
+ ? null
1115
+ : html`<slot
1116
+ name="jh-input-right"
1117
+ @slotchange=${this.#handleSlotChange}
1118
+ ></slot>
1119
+ `;
1120
+
1121
+ return html`${leftSlot}${clearBtn}${rightSlot}`;
1122
+ }
1123
+
1124
+ #getDescribedby() {
1125
+ let describedbyString = '';
1126
+
1127
+ if (this.errorText) {
1128
+ describedbyString += `jh-input-error-${this.#id}`;
1129
+ }
1130
+ if (this.helperText) {
1131
+ describedbyString += ` jh-input-helper-${this.#id}`;
1132
+ }
1133
+ if (this.showCharCount) {
1134
+ describedbyString += ` jh-input-counter-${this.#id}`;
1135
+ }
1136
+ return describedbyString;
1137
+ }
1138
+
1139
+ render() {
1140
+ let label;
1141
+ let indicator;
1142
+ let helperText;
1143
+ let input;
1144
+ let footer;
1145
+ let errorText;
1146
+ let charCount;
1147
+ let describedby;
1148
+
1149
+ if (this.label) {
1150
+ if (this.showIndicator) {
1151
+ if (this.required) {
1152
+ indicator = html`<span class="indicator" aria-hidden="true"> *</span>`;
1153
+ } else {
1154
+ indicator = html`<span class="indicator"> (optional)</span>`;
1155
+ }
1156
+ }
1157
+
1158
+ if (this.helperText) {
1159
+ helperText = html`
1160
+ <p id="jh-input-helper-${this.#id}" class="helper-text">
1161
+ ${this.helperText}
1162
+ </p>
1163
+ `;
1164
+ }
1165
+
1166
+ label = html`
1167
+ <label for="jh-input-${this.#id}">${this.label}${indicator}</label>
1168
+ ${helperText}
1169
+ `;
1170
+ }
1171
+
1172
+ if (this.showCharCount) {
1173
+ let valueLength = this.value ? this.value.length : 0;
1174
+
1175
+ let charCountValue = `${
1176
+ this.maxlength ? `${valueLength}/${this.maxlength}` : valueLength
1177
+ }`;
1178
+
1179
+ if (valueLength && valueLength === Number(this.maxlength)) {
1180
+ this.#handleMaxlength();
1181
+ }
1182
+
1183
+ charCount = html`
1184
+ <p class="counter" aria-hidden="true">${charCountValue}</p>
1185
+ `;
1186
+ }
1187
+
1188
+ if (this.invalid && this.errorText) {
1189
+ errorText = html`
1190
+ <p id="jh-input-error-${this.#id}" class="error-text">
1191
+ ${this.errorText}
1192
+ </p>
1193
+ `;
1194
+ }
1195
+
1196
+ if ((this.invalid && this.errorText) || this.showCharCount) {
1197
+ footer = html`
1198
+ <div class="footer-content">
1199
+ ${errorText}
1200
+ ${charCount}
1201
+ </div>
1202
+ `;
1203
+ }
1204
+
1205
+ if (helperText || errorText || charCount) {
1206
+ describedby = this.#getDescribedby();
1207
+ }
1208
+
1209
+ input = html`
1210
+ <div class="input-container">
1211
+ <input
1212
+ id="jh-input-${this.#id}"
1213
+ aria-describedby=${describedby}
1214
+ aria-invalid=${ifDefined(this.invalid ? 'true' : null)}
1215
+ aria-label=${ifDefined(
1216
+ this.accessibleLabel === '' ? null : this.accessibleLabel
1217
+ )}
1218
+ autocomplete=${ifDefined(
1219
+ this.autocomplete === '' ? null : this.autocomplete
1220
+ )}
1221
+ ?disabled=${this.disabled}
1222
+ enterkeyhint=${ifDefined(
1223
+ this.enterkeyhint === '' ? null : this.enterkeyhint
1224
+ )}
1225
+ inputmode=${ifDefined(this.inputmode === '' ? null : this.inputmode)}
1226
+ maxlength=${ifDefined(this.maxlength === '' ? null : this.maxlength)}
1227
+ minlength=${ifDefined(this.minlength === '' ? null : this.minlength)}
1228
+ name=${ifDefined(this.name === '' ? null : this.name)}
1229
+ ?readonly=${this.readonly}
1230
+ ?required=${this.required}
1231
+ type="text"
1232
+ .value=${this.value}
1233
+ @keydown=${this.inputMask ? this.#handleKeydown : null}
1234
+ @change=${this.#handleChange}
1235
+ @input=${this.#handleInput}
1236
+ @select=${this.#handleSelect}
1237
+ />${this.#getSlots()}
1238
+ </div>
1239
+ `;
1240
+
1241
+ return html`
1242
+ ${label} ${input} ${footer}
1243
+ `;
1244
+ }
1245
+ }
1246
+
1247
+ customElements.define('jh-input', JhInput);