@dile/ui 2.26.0 → 2.27.0
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.
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DileOtpInput } from './src/DileOtpInput.js';
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { LitElement, html, css } from 'lit';
|
|
2
|
+
import { DileEmmitChange } from '../../../mixins/form/index.js';
|
|
3
|
+
import { messageStyles } from '../../input/index.js';
|
|
4
|
+
import { sanitizeOtpValue } from '../../../lib/otp/sanitizeOtpValue.js';
|
|
5
|
+
|
|
6
|
+
export class DileOtpInput extends DileEmmitChange(LitElement) {
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Fired when user press enter key.
|
|
10
|
+
*
|
|
11
|
+
* @event enter-pressed
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Fired when value changes, alongside the generic `element-changed` event.
|
|
16
|
+
*
|
|
17
|
+
* @event dile-otp-input-changed
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Fired once, when the last empty box has just been filled and the code is complete.
|
|
22
|
+
*
|
|
23
|
+
* @event dile-otp-input-completed
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
static get properties() {
|
|
27
|
+
return {
|
|
28
|
+
/** Label to the element */
|
|
29
|
+
label: { type: String },
|
|
30
|
+
|
|
31
|
+
/** Number of characters (boxes) of the code */
|
|
32
|
+
length: { type: Number },
|
|
33
|
+
|
|
34
|
+
/** Set initial value to the input. This property syncs to each box value */
|
|
35
|
+
value: { type: String },
|
|
36
|
+
|
|
37
|
+
/** Name for this input field */
|
|
38
|
+
name: { type: String },
|
|
39
|
+
|
|
40
|
+
/** Disable the input field */
|
|
41
|
+
disabled: { type: Boolean },
|
|
42
|
+
|
|
43
|
+
/** ReadOnly attribute */
|
|
44
|
+
readonly: { type: Boolean },
|
|
45
|
+
|
|
46
|
+
/** Has error on this input field */
|
|
47
|
+
errored: { type: Boolean },
|
|
48
|
+
|
|
49
|
+
/** Message Displayed */
|
|
50
|
+
message: { type: String },
|
|
51
|
+
|
|
52
|
+
/** Hide errors on input */
|
|
53
|
+
hideErrorOnInput: { type: Boolean },
|
|
54
|
+
|
|
55
|
+
/** Set the application focus to this the input component after the initialization */
|
|
56
|
+
focusOnStart: { type: Boolean },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static get formAssociated() {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
constructor() {
|
|
65
|
+
super();
|
|
66
|
+
this.label = '';
|
|
67
|
+
this.value = '';
|
|
68
|
+
this.name = '';
|
|
69
|
+
this.length = 6;
|
|
70
|
+
this.disabled = false;
|
|
71
|
+
this.internals = this.attachInternals();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
firstUpdated() {
|
|
75
|
+
if (this.focusOnStart) {
|
|
76
|
+
this.focus();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
updated(changedProperties) {
|
|
81
|
+
if (changedProperties.has('value')) {
|
|
82
|
+
this.emmitChange();
|
|
83
|
+
this.internals.setFormValue(this.value);
|
|
84
|
+
this.dispatchEvent(new CustomEvent('dile-otp-input-changed', {
|
|
85
|
+
bubbles: true,
|
|
86
|
+
composed: true,
|
|
87
|
+
detail: {
|
|
88
|
+
name: this.name,
|
|
89
|
+
value: this.value
|
|
90
|
+
}
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
const previousValue = changedProperties.get('value') || '';
|
|
94
|
+
if (this.value.length === this.length && previousValue.length !== this.length) {
|
|
95
|
+
this.dispatchEvent(new CustomEvent('dile-otp-input-completed', {
|
|
96
|
+
bubbles: true,
|
|
97
|
+
composed: true,
|
|
98
|
+
detail: {
|
|
99
|
+
name: this.name,
|
|
100
|
+
value: this.value
|
|
101
|
+
}
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
static get styles() {
|
|
108
|
+
return [
|
|
109
|
+
messageStyles,
|
|
110
|
+
css`
|
|
111
|
+
* {
|
|
112
|
+
box-sizing: border-box;
|
|
113
|
+
}
|
|
114
|
+
:host {
|
|
115
|
+
display: block;
|
|
116
|
+
margin-bottom: 10px;
|
|
117
|
+
}
|
|
118
|
+
main {
|
|
119
|
+
width: var(--dile-otp-input-section-width, auto);
|
|
120
|
+
}
|
|
121
|
+
label {
|
|
122
|
+
display: block;
|
|
123
|
+
margin-bottom: var(--dile-otp-input-label-margin-bottom, 4px);
|
|
124
|
+
font-size: var(--dile-otp-input-label-font-size, 1em);
|
|
125
|
+
color: var(--dile-otp-input-label-color, #59e);
|
|
126
|
+
font-weight: var(--dile-otp-input-label-font-weight, normal);
|
|
127
|
+
}
|
|
128
|
+
section.for-input {
|
|
129
|
+
display: flex;
|
|
130
|
+
gap: var(--dile-otp-input-gap, 8px);
|
|
131
|
+
}
|
|
132
|
+
input {
|
|
133
|
+
box-sizing: border-box;
|
|
134
|
+
width: var(--dile-otp-input-box-size, 2.5em);
|
|
135
|
+
height: var(--dile-otp-input-box-size, 2.5em);
|
|
136
|
+
padding: 0;
|
|
137
|
+
text-align: center;
|
|
138
|
+
border-radius: var(--dile-otp-input-border-radius, 5px);
|
|
139
|
+
border: var(--dile-otp-input-border-width, 1px) solid var(--dile-otp-input-border-color, #888);
|
|
140
|
+
font-size: var(--dile-otp-input-font-size, 1.25em);
|
|
141
|
+
background-color: var(--dile-otp-input-background-color, #fff);
|
|
142
|
+
color: var(--dile-otp-input-color, #303030);
|
|
143
|
+
}
|
|
144
|
+
input:focus {
|
|
145
|
+
outline: none;
|
|
146
|
+
border-color: var(--dile-otp-input-focus-border-color, #6af);
|
|
147
|
+
}
|
|
148
|
+
input:disabled {
|
|
149
|
+
background-color: #f5f5f5;
|
|
150
|
+
border-color: var(--dile-otp-input-disabled-border-color, #eee);
|
|
151
|
+
}
|
|
152
|
+
.errored {
|
|
153
|
+
border-color: var(--dile-otp-input-error-border-color, #c00);
|
|
154
|
+
background-color: var(--dile-otp-input-error-background-color, var(--dile-otp-input-background-color, #fff));
|
|
155
|
+
}
|
|
156
|
+
`
|
|
157
|
+
];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
render() {
|
|
161
|
+
return html`
|
|
162
|
+
<main>
|
|
163
|
+
${this.label
|
|
164
|
+
? html`<label>${this.label}</label>`
|
|
165
|
+
: ''}
|
|
166
|
+
<section class="for-input">
|
|
167
|
+
${this.boxesChars.map((char, index) => html`
|
|
168
|
+
<input
|
|
169
|
+
type="text"
|
|
170
|
+
inputmode="numeric"
|
|
171
|
+
pattern="[0-9]*"
|
|
172
|
+
maxlength="1"
|
|
173
|
+
autocomplete="${index === 0 ? 'one-time-code' : 'off'}"
|
|
174
|
+
?disabled="${this.disabled}"
|
|
175
|
+
?readonly="${this.readonly}"
|
|
176
|
+
.value="${char}"
|
|
177
|
+
class="${this.errored ? 'errored' : ''}"
|
|
178
|
+
@input="${(e) => this._onInput(e, index)}"
|
|
179
|
+
@keydown="${(e) => this._onKeydown(e, index)}"
|
|
180
|
+
@keypress="${this._lookForEnter}"
|
|
181
|
+
@paste="${(e) => this._onPaste(e)}"
|
|
182
|
+
@focus="${this._onFocus}"
|
|
183
|
+
/>
|
|
184
|
+
`)}
|
|
185
|
+
</section>
|
|
186
|
+
${this.messageTemplate}
|
|
187
|
+
</main>
|
|
188
|
+
`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
get messageTemplate() {
|
|
192
|
+
return html`
|
|
193
|
+
${this.message
|
|
194
|
+
? html`<div class="message ${this.errored ? 'errored-msg' : ''}"><span>${this.message}</span></div>`
|
|
195
|
+
: ''
|
|
196
|
+
}
|
|
197
|
+
`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The current value split into one character per box, padded with empty strings.
|
|
202
|
+
*/
|
|
203
|
+
get boxesChars() {
|
|
204
|
+
return Array.from({ length: this.length }, (_, index) => this.value.charAt(index));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The rendered box input elements, in order.
|
|
209
|
+
*/
|
|
210
|
+
get boxes() {
|
|
211
|
+
return Array.from(this.shadowRoot.querySelectorAll('input'));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
_setChar(index, char) {
|
|
215
|
+
const chars = this.boxesChars;
|
|
216
|
+
chars[index] = char;
|
|
217
|
+
this.value = chars.join('');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
_onInput(e, index) {
|
|
221
|
+
const raw = e.target.value;
|
|
222
|
+
|
|
223
|
+
// Autofill (e.g. iOS/Android SMS autofill) can drop the whole code into a single box
|
|
224
|
+
if (raw.length > 1) {
|
|
225
|
+
const digits = sanitizeOtpValue(raw, this.length);
|
|
226
|
+
if (digits) {
|
|
227
|
+
if (this.hideErrorOnInput && this.errored) {
|
|
228
|
+
this.clearError();
|
|
229
|
+
}
|
|
230
|
+
this.value = digits;
|
|
231
|
+
this._focusBox(Math.min(digits.length, this.length - 1));
|
|
232
|
+
} else {
|
|
233
|
+
e.target.value = this.boxesChars[index];
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const digit = raw.replace(/\D/g, '');
|
|
239
|
+
if (!digit && raw !== '') {
|
|
240
|
+
e.target.value = this.boxesChars[index];
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (this.hideErrorOnInput && this.errored) {
|
|
245
|
+
this.clearError();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
this._setChar(index, digit);
|
|
249
|
+
|
|
250
|
+
if (digit && index < this.length - 1) {
|
|
251
|
+
this._focusBox(index + 1);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
_onKeydown(e, index) {
|
|
256
|
+
if (e.key === 'Backspace' && !this.boxesChars[index] && index > 0) {
|
|
257
|
+
e.preventDefault();
|
|
258
|
+
this._focusBox(index - 1);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (e.key === 'ArrowLeft' && index > 0) {
|
|
262
|
+
e.preventDefault();
|
|
263
|
+
this._focusBox(index - 1);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (e.key === 'ArrowRight' && index < this.length - 1) {
|
|
267
|
+
e.preventDefault();
|
|
268
|
+
this._focusBox(index + 1);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (e.key.length === 1 && !/[0-9]/.test(e.key) && !e.ctrlKey && !e.metaKey) {
|
|
272
|
+
e.preventDefault();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
_onPaste(e) {
|
|
277
|
+
e.preventDefault();
|
|
278
|
+
const pasted = (e.clipboardData || window.clipboardData).getData('text');
|
|
279
|
+
const digits = sanitizeOtpValue(pasted, this.length);
|
|
280
|
+
if (!digits) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (this.hideErrorOnInput && this.errored) {
|
|
284
|
+
this.clearError();
|
|
285
|
+
}
|
|
286
|
+
this.value = digits;
|
|
287
|
+
this._focusBox(Math.min(digits.length, this.length - 1));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
_onFocus(e) {
|
|
291
|
+
e.target.select();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
_focusBox(index) {
|
|
295
|
+
const input = this.boxes[index];
|
|
296
|
+
if (input) {
|
|
297
|
+
input.focus();
|
|
298
|
+
input.select();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Private method to dispatch events on enter key pressed
|
|
304
|
+
*/
|
|
305
|
+
_lookForEnter(e) {
|
|
306
|
+
let keycode = (e.keyCode ? e.keyCode : e.which);
|
|
307
|
+
if (keycode == '13') {
|
|
308
|
+
this.dispatchEvent(new CustomEvent('enter-pressed'));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
clearError() {
|
|
313
|
+
this.errored = false;
|
|
314
|
+
this.message = '';
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
focus() {
|
|
318
|
+
const emptyIndex = this.boxesChars.findIndex((char) => !char);
|
|
319
|
+
this._focusBox(emptyIndex === -1 ? this.length - 1 : emptyIndex);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
clear() {
|
|
323
|
+
this.value = '';
|
|
324
|
+
this.focus();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { sanitizeOtpValue } from './sanitizeOtpValue.js';
|
|
4
|
+
|
|
5
|
+
test('returns empty string for empty input', () => {
|
|
6
|
+
assert.equal(sanitizeOtpValue('', 6), '');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test('strips non-digit characters', () => {
|
|
10
|
+
assert.equal(sanitizeOtpValue('12 34-56', 6), '123456');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('truncates to the given length', () => {
|
|
14
|
+
assert.equal(sanitizeOtpValue('1234567890', 6), '123456');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('returns empty string for null or undefined', () => {
|
|
18
|
+
assert.equal(sanitizeOtpValue(null, 6), '');
|
|
19
|
+
assert.equal(sanitizeOtpValue(undefined, 6), '');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('keeps digits shorter than length untouched', () => {
|
|
23
|
+
assert.equal(sanitizeOtpValue('12', 6), '12');
|
|
24
|
+
});
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dile/ui",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.27.0",
|
|
4
4
|
"description": "UI Core components from dile-components.",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
6
7
|
"scripts": {
|
|
7
8
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
9
|
},
|
|
@@ -26,5 +27,5 @@
|
|
|
26
27
|
"publishConfig": {
|
|
27
28
|
"access": "public"
|
|
28
29
|
},
|
|
29
|
-
"gitHead": "
|
|
30
|
+
"gitHead": "5c04395f81c01fd289869b1b99d427d482c516a5"
|
|
30
31
|
}
|