@unlk/keymaster 1.6.7 → 1.6.8
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.
- package/CHANGELOG.md +5 -0
- package/dist/css/keymaster.css +173 -4
- package/dist/css/keymaster.css.map +1 -1
- package/dist/css/keymaster.min.css +1 -1
- package/dist/js/keymaster.js +183 -13
- package/dist/js/keymaster.js.map +1 -1
- package/dist/js/keymaster.min.js +14 -14
- package/dist/js/keymaster.min.js.map +1 -1
- package/js/bootstrap.js +1 -0
- package/js/otp-input.js +199 -0
- package/package.json +1 -1
- package/react/index.js +1 -0
- package/react/use-otp-input.js +129 -0
- package/scss/theme/_forms.scss +1 -0
- package/scss/theme/_version.scss +1 -1
- package/scss/theme/forms/_otp.scss +143 -0
- package/scss/theme/forms/_validation.scss +6 -1
- package/scss/theme/mixins/_forms.scss +17 -0
package/js/bootstrap.js
CHANGED
|
@@ -15,3 +15,4 @@ export { default as CarouselCaption } from './carousel-caption';
|
|
|
15
15
|
export { default as CarouselHeight } from './carousel-height';
|
|
16
16
|
export { default as VideoModal } from './video-modal';
|
|
17
17
|
export { default as AccordionScroll } from './accordion-scroll';
|
|
18
|
+
export { default as OtpInput } from './otp-input';
|
package/js/otp-input.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import BaseComponent from 'bootstrap/js/src/base-component.js';
|
|
2
|
+
import EventHandler from 'bootstrap/js/src/dom/event-handler.js';
|
|
3
|
+
import SelectorEngine from 'bootstrap/js/src/dom/selector-engine.js';
|
|
4
|
+
import { defineJQueryPlugin } from 'bootstrap/js/src/util/index.js';
|
|
5
|
+
|
|
6
|
+
const NAME = 'otpInput';
|
|
7
|
+
const DATA_KEY = 'bs.otpInput';
|
|
8
|
+
const EVENT_KEY = `.${DATA_KEY}`;
|
|
9
|
+
|
|
10
|
+
const EVENT_INPUT = `input${EVENT_KEY}`;
|
|
11
|
+
const EVENT_COMPLETE = `complete${EVENT_KEY}`;
|
|
12
|
+
|
|
13
|
+
const SELECTOR_OTP = '[data-bs-otp]';
|
|
14
|
+
const SELECTOR_INPUT = '.otp-input';
|
|
15
|
+
const CLASS_SLOTS = 'otp-slots';
|
|
16
|
+
const CLASS_GROUP = 'otp-group';
|
|
17
|
+
const CLASS_SLOT = 'otp-slot';
|
|
18
|
+
const CLASS_CHAR = 'otp-char';
|
|
19
|
+
const CLASS_CURSOR = 'otp-cursor';
|
|
20
|
+
const CLASS_SEPARATOR = 'otp-separator';
|
|
21
|
+
const CLASS_SLOT_ACTIVE = 'otp-slot-active';
|
|
22
|
+
const CLASS_SLOT_FILLED = 'otp-slot-filled';
|
|
23
|
+
const CLASS_DISABLED = 'disabled';
|
|
24
|
+
|
|
25
|
+
const PATTERNS = {
|
|
26
|
+
numeric: /\D/g,
|
|
27
|
+
alpha: /[^a-zA-Z]/g,
|
|
28
|
+
alphanumeric: /[^a-zA-Z0-9]/g
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const INPUT_PATTERNS = {
|
|
32
|
+
numeric: '[0-9]',
|
|
33
|
+
alpha: '[a-zA-Z]',
|
|
34
|
+
alphanumeric: '[a-zA-Z0-9]'
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
class OtpInput extends BaseComponent {
|
|
38
|
+
constructor(element, config) {
|
|
39
|
+
super(element, config);
|
|
40
|
+
|
|
41
|
+
this._input = SelectorEngine.findOne(SELECTOR_INPUT, this._element);
|
|
42
|
+
if (!this._input) return;
|
|
43
|
+
|
|
44
|
+
this._length = this._input.maxLength > 0 ? this._input.maxLength : 6;
|
|
45
|
+
this._type = this._element.dataset.bsType || 'numeric';
|
|
46
|
+
const charPattern = INPUT_PATTERNS[this._type] || INPUT_PATTERNS.numeric;
|
|
47
|
+
this._input.pattern = `${charPattern}{${this._length}}`;
|
|
48
|
+
this._mask = this._element.dataset.bsMask === 'true';
|
|
49
|
+
this._groups = this._element.dataset.bsGroups
|
|
50
|
+
? String(this._element.dataset.bsGroups).split(',').map(Number)
|
|
51
|
+
: [this._length];
|
|
52
|
+
|
|
53
|
+
this._slots = [];
|
|
54
|
+
this._buildSlots();
|
|
55
|
+
this._bindEvents();
|
|
56
|
+
this._render();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Public API
|
|
60
|
+
|
|
61
|
+
getValue() {
|
|
62
|
+
return this._input.value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
setValue(value) {
|
|
66
|
+
const pattern = PATTERNS[this._type] || PATTERNS.numeric;
|
|
67
|
+
this._input.value = String(value).replace(pattern, '').slice(0, this._length);
|
|
68
|
+
this._render();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
clear() {
|
|
72
|
+
this._input.value = '';
|
|
73
|
+
this._render();
|
|
74
|
+
this._input.focus();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Private
|
|
78
|
+
|
|
79
|
+
_buildSlots() {
|
|
80
|
+
// Hide real input visually but keep it accessible
|
|
81
|
+
this._input.style.cssText =
|
|
82
|
+
'position:absolute;opacity:0;pointer-events:none;width:1px;height:1px;overflow:hidden;white-space:nowrap;';
|
|
83
|
+
|
|
84
|
+
const slotsWrapper = document.createElement('div');
|
|
85
|
+
slotsWrapper.classList.add(CLASS_SLOTS);
|
|
86
|
+
slotsWrapper.setAttribute('aria-hidden', 'true');
|
|
87
|
+
|
|
88
|
+
let slotIndex = 0;
|
|
89
|
+
for (const [gi, groupLen] of this._groups.entries()) {
|
|
90
|
+
const groupEl = document.createElement('div');
|
|
91
|
+
groupEl.classList.add(CLASS_GROUP);
|
|
92
|
+
|
|
93
|
+
for (let i = 0; i < groupLen; i++) {
|
|
94
|
+
const slot = document.createElement('div');
|
|
95
|
+
slot.classList.add(CLASS_SLOT);
|
|
96
|
+
slot.dataset.index = slotIndex;
|
|
97
|
+
|
|
98
|
+
const char = document.createElement('span');
|
|
99
|
+
char.classList.add(CLASS_CHAR);
|
|
100
|
+
|
|
101
|
+
const cursor = document.createElement('span');
|
|
102
|
+
cursor.classList.add(CLASS_CURSOR);
|
|
103
|
+
|
|
104
|
+
slot.append(char, cursor);
|
|
105
|
+
groupEl.append(slot);
|
|
106
|
+
this._slots.push({ slot, char });
|
|
107
|
+
slotIndex++;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
slotsWrapper.append(groupEl);
|
|
111
|
+
|
|
112
|
+
if (gi < this._groups.length - 1) {
|
|
113
|
+
const sep = document.createElement('span');
|
|
114
|
+
sep.classList.add(CLASS_SEPARATOR);
|
|
115
|
+
sep.textContent = '–';
|
|
116
|
+
slotsWrapper.append(sep);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this._element.append(slotsWrapper);
|
|
121
|
+
this._slotsWrapper = slotsWrapper;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
_bindEvents() {
|
|
125
|
+
EventHandler.on(this._slotsWrapper, 'click', () => {
|
|
126
|
+
if (this._input.disabled) return;
|
|
127
|
+
this._input.focus();
|
|
128
|
+
const len = this._input.value.length;
|
|
129
|
+
this._input.setSelectionRange(len, len);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
EventHandler.on(this._input, 'focus', () => this._render());
|
|
133
|
+
EventHandler.on(this._input, 'blur', () => this._render());
|
|
134
|
+
|
|
135
|
+
EventHandler.on(this._input, 'input', () => {
|
|
136
|
+
const pattern = PATTERNS[this._type] || PATTERNS.numeric;
|
|
137
|
+
this._input.value = this._input.value.replace(pattern, '').slice(0, this._length);
|
|
138
|
+
this._render();
|
|
139
|
+
|
|
140
|
+
EventHandler.trigger(this._element, EVENT_INPUT, { value: this._input.value });
|
|
141
|
+
|
|
142
|
+
if (this._input.value.length === this._length) {
|
|
143
|
+
EventHandler.trigger(this._element, EVENT_COMPLETE, { value: this._input.value });
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
EventHandler.on(this._input, 'keydown', (e) => {
|
|
148
|
+
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
|
|
149
|
+
requestAnimationFrame(() => this._render());
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
_render() {
|
|
155
|
+
const val = this._input.value;
|
|
156
|
+
const focused = document.activeElement === this._input;
|
|
157
|
+
const activeIdx = focused ? Math.min(val.length, this._length - 1) : -1;
|
|
158
|
+
const isDisabled = this._input.disabled;
|
|
159
|
+
|
|
160
|
+
this._slotsWrapper.classList.toggle(CLASS_DISABLED, isDisabled);
|
|
161
|
+
|
|
162
|
+
for (const [i, { slot, char }] of this._slots.entries()) {
|
|
163
|
+
const ch = val[i] || '';
|
|
164
|
+
char.textContent = this._mask && ch ? '•' : ch;
|
|
165
|
+
slot.classList.toggle(CLASS_SLOT_ACTIVE, i === activeIdx && !isDisabled);
|
|
166
|
+
slot.classList.toggle(CLASS_SLOT_FILLED, Boolean(ch));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Static
|
|
171
|
+
|
|
172
|
+
static get NAME() {
|
|
173
|
+
return NAME;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
static jQueryInterface(config, ...args) {
|
|
177
|
+
return this.each(function () {
|
|
178
|
+
const data = OtpInput.getOrCreateInstance(this);
|
|
179
|
+
if (typeof config === 'string') {
|
|
180
|
+
if (typeof data[config] === 'undefined') {
|
|
181
|
+
throw new TypeError(`No method named "${config}"`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
data[config](...args);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Auto-init on DOMContentLoaded
|
|
191
|
+
EventHandler.on(globalThis, 'load', () => {
|
|
192
|
+
for (const el of SelectorEngine.find(SELECTOR_OTP)) {
|
|
193
|
+
OtpInput.getOrCreateInstance(el);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
defineJQueryPlugin(OtpInput);
|
|
198
|
+
|
|
199
|
+
export default OtpInput;
|
package/package.json
CHANGED
package/react/index.js
CHANGED
|
@@ -3,3 +3,4 @@ export { useCarouselCaption } from './use-carousel-caption.js';
|
|
|
3
3
|
export { useCarouselHeight } from './use-carousel-height.js';
|
|
4
4
|
export { useVideoModal } from './use-video-modal.js';
|
|
5
5
|
export { useAccordionScroll } from './use-accordion-scroll.js';
|
|
6
|
+
export { useOtpInput } from './use-otp-input.js';
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { useState, useRef, useCallback, useId } from 'react';
|
|
2
|
+
|
|
3
|
+
const PATTERNS = {
|
|
4
|
+
numeric: /\D/g,
|
|
5
|
+
alpha: /[^a-zA-Z]/g,
|
|
6
|
+
alphanumeric: /[^a-zA-Z0-9]/g,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Replicates otp-input.js behaviour in React.
|
|
11
|
+
*
|
|
12
|
+
* Manages a single hidden input whose value is rendered as individual
|
|
13
|
+
* character slots, matching the keymaster OTP plugin's visual output.
|
|
14
|
+
*
|
|
15
|
+
* @param {object} options
|
|
16
|
+
* @param {number} [options.length=6] Number of character slots.
|
|
17
|
+
* @param {'numeric'|'alphanumeric'|'alpha'} [options.type='numeric']
|
|
18
|
+
* @param {boolean} [options.mask=false] Obscure entered characters.
|
|
19
|
+
* @param {function} [options.onComplete] Called with the value when all slots are filled.
|
|
20
|
+
* @param {function} [options.onChange] Called with the value on every change.
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* const otp = useOtpInput({ length: 6, onComplete: (val) => console.log(val) });
|
|
24
|
+
*
|
|
25
|
+
* <div className="otp-wrapper">
|
|
26
|
+
* <label className="form-label">One-time password</label>
|
|
27
|
+
* <div className="otp" onClick={otp.focusInput}>
|
|
28
|
+
* <input {...otp.inputProps} />
|
|
29
|
+
* <div className="otp-slots" aria-hidden="true">
|
|
30
|
+
* {otp.slots.map((slot, i) => (
|
|
31
|
+
* <div key={i} className="otp-group">
|
|
32
|
+
* <div className={otp.slotClassName(i)}>
|
|
33
|
+
* <span className="otp-char">{slot.display}</span>
|
|
34
|
+
* {slot.active && <span className="otp-cursor" />}
|
|
35
|
+
* </div>
|
|
36
|
+
* </div>
|
|
37
|
+
* ))}
|
|
38
|
+
* </div>
|
|
39
|
+
* </div>
|
|
40
|
+
* </div>
|
|
41
|
+
*/
|
|
42
|
+
export function useOtpInput({
|
|
43
|
+
length = 6,
|
|
44
|
+
type = 'numeric',
|
|
45
|
+
mask = false,
|
|
46
|
+
onComplete,
|
|
47
|
+
onChange,
|
|
48
|
+
} = {}) {
|
|
49
|
+
const [value, setValue] = useState('');
|
|
50
|
+
const [focused, setFocused] = useState(false);
|
|
51
|
+
const inputRef = useRef(null);
|
|
52
|
+
const id = useId();
|
|
53
|
+
const pattern = PATTERNS[type] || PATTERNS.numeric;
|
|
54
|
+
|
|
55
|
+
const handleInput = useCallback(
|
|
56
|
+
(e) => {
|
|
57
|
+
const next = e.target.value.replace(pattern, '').slice(0, length);
|
|
58
|
+
setValue(next);
|
|
59
|
+
onChange?.(next);
|
|
60
|
+
if (next.length === length) onComplete?.(next);
|
|
61
|
+
},
|
|
62
|
+
[length, pattern, onChange, onComplete]
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const focusInput = useCallback(() => {
|
|
66
|
+
inputRef.current?.focus();
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
const clear = useCallback(() => {
|
|
70
|
+
setValue('');
|
|
71
|
+
inputRef.current?.focus();
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
// Derive per-slot display data
|
|
75
|
+
const activeIdx = focused ? Math.min(value.length, length - 1) : -1;
|
|
76
|
+
const slots = Array.from({ length }, (_, i) => {
|
|
77
|
+
const ch = value[i] || '';
|
|
78
|
+
return {
|
|
79
|
+
char: ch,
|
|
80
|
+
display: mask && ch ? '•' : ch,
|
|
81
|
+
active: i === activeIdx,
|
|
82
|
+
filled: Boolean(ch),
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const slotClassName = useCallback(
|
|
87
|
+
(i) => {
|
|
88
|
+
const s = slots[i];
|
|
89
|
+
return [
|
|
90
|
+
'otp-slot',
|
|
91
|
+
s?.active ? 'otp-slot-active' : '',
|
|
92
|
+
s?.filled ? 'otp-slot-filled' : '',
|
|
93
|
+
]
|
|
94
|
+
.filter(Boolean)
|
|
95
|
+
.join(' ');
|
|
96
|
+
},
|
|
97
|
+
[slots]
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const inputProps = {
|
|
101
|
+
ref: inputRef,
|
|
102
|
+
type: 'text',
|
|
103
|
+
className: 'otp-input',
|
|
104
|
+
id,
|
|
105
|
+
value,
|
|
106
|
+
maxLength: length,
|
|
107
|
+
autoComplete: 'one-time-code',
|
|
108
|
+
inputMode: type === 'numeric' ? 'numeric' : 'text',
|
|
109
|
+
onInput: handleInput,
|
|
110
|
+
// Keep value in sync when controlled
|
|
111
|
+
onChange(e) {
|
|
112
|
+
const next = e.target.value.replace(pattern, '').slice(0, length);
|
|
113
|
+
setValue(next);
|
|
114
|
+
},
|
|
115
|
+
onFocus: () => setFocused(true),
|
|
116
|
+
onBlur: () => setFocused(false),
|
|
117
|
+
style: {
|
|
118
|
+
position: 'absolute',
|
|
119
|
+
opacity: 0,
|
|
120
|
+
pointerEvents: 'none',
|
|
121
|
+
width: 1,
|
|
122
|
+
height: 1,
|
|
123
|
+
overflow: 'hidden',
|
|
124
|
+
whiteSpace: 'nowrap',
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return { value, slots, inputProps, slotClassName, focusInput, clear, inputId: id };
|
|
129
|
+
}
|
package/scss/theme/_forms.scss
CHANGED
package/scss/theme/_version.scss
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// GENERATED FILE – do not edit manually
|
|
2
|
-
$km-version: "1.6.
|
|
2
|
+
$km-version: "1.6.8" !default;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// OTP Input — single hidden <input> rendered as visual character slots.
|
|
2
|
+
// Slots deliberately match .form-control styling using the same $input-* tokens.
|
|
3
|
+
|
|
4
|
+
// Wrapper that holds the label + .otp container
|
|
5
|
+
.otp-wrapper {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
gap: .375rem;
|
|
9
|
+
align-items: flex-start;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.otp {
|
|
13
|
+
position: relative;
|
|
14
|
+
display: inline-block;
|
|
15
|
+
|
|
16
|
+
// The real input — hidden but accessible
|
|
17
|
+
.otp-input {
|
|
18
|
+
position: absolute;
|
|
19
|
+
width: 1px;
|
|
20
|
+
height: 1px;
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
white-space: nowrap;
|
|
23
|
+
pointer-events: none;
|
|
24
|
+
opacity: 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Visual slot container
|
|
28
|
+
.otp-slots {
|
|
29
|
+
display: flex;
|
|
30
|
+
gap: .375rem;
|
|
31
|
+
align-items: center;
|
|
32
|
+
cursor: text;
|
|
33
|
+
user-select: none;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Each digit group (used when data-bs-groups is set)
|
|
37
|
+
.otp-group {
|
|
38
|
+
display: flex;
|
|
39
|
+
gap: .375rem;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Individual character slot — same look as .form-control
|
|
43
|
+
.otp-slot {
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
justify-content: center;
|
|
47
|
+
width: $input-height; // square cell using the standard input height
|
|
48
|
+
height: $input-height;
|
|
49
|
+
font-family: $input-font-family;
|
|
50
|
+
font-size: $input-font-size;
|
|
51
|
+
font-weight: $font-weight-semibold;
|
|
52
|
+
color: $input-color;
|
|
53
|
+
background-color: $input-bg;
|
|
54
|
+
border: $input-border-width solid $input-border-color;
|
|
55
|
+
@include border-radius($input-border-radius);
|
|
56
|
+
@include transition(border-color .15s ease-in-out, box-shadow .15s ease-in-out);
|
|
57
|
+
|
|
58
|
+
&.otp-slot-active {
|
|
59
|
+
border-color: $input-focus-border-color;
|
|
60
|
+
outline: 0;
|
|
61
|
+
box-shadow: $input-focus-box-shadow;
|
|
62
|
+
|
|
63
|
+
.otp-cursor {
|
|
64
|
+
animation: otp-blink 1s step-end infinite;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
&.otp-slot-filled .otp-cursor {
|
|
69
|
+
display: none;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.otp-char {
|
|
74
|
+
line-height: 1;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Blinking text cursor
|
|
78
|
+
.otp-cursor {
|
|
79
|
+
position: absolute;
|
|
80
|
+
width: 1px;
|
|
81
|
+
height: 1.25em;
|
|
82
|
+
background-color: currentcolor;
|
|
83
|
+
opacity: 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Separator between groups
|
|
87
|
+
.otp-separator {
|
|
88
|
+
margin: 0 .25rem;
|
|
89
|
+
font-size: $input-font-size;
|
|
90
|
+
line-height: $input-height;
|
|
91
|
+
color: var(--#{$prefix}secondary-color);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Disabled state — mirrors .form-control:disabled
|
|
95
|
+
&:has(.otp-slots.disabled) {
|
|
96
|
+
.otp-slots {
|
|
97
|
+
pointer-events: none;
|
|
98
|
+
cursor: default;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.otp-slot {
|
|
102
|
+
color: $input-disabled-color;
|
|
103
|
+
background-color: $input-disabled-bg;
|
|
104
|
+
border-color: $input-disabled-border-color;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Small — matches .form-control-sm
|
|
109
|
+
&.otp-sm {
|
|
110
|
+
.otp-slot {
|
|
111
|
+
width: $input-height-sm;
|
|
112
|
+
height: $input-height-sm;
|
|
113
|
+
font-size: $input-font-size-sm;
|
|
114
|
+
@include border-radius($input-border-radius-sm);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.otp-separator {
|
|
118
|
+
font-size: $input-font-size-sm;
|
|
119
|
+
line-height: $input-height-sm;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Large — matches .form-control-lg
|
|
124
|
+
&.otp-lg {
|
|
125
|
+
.otp-slot {
|
|
126
|
+
width: $input-height-lg;
|
|
127
|
+
height: $input-height-lg;
|
|
128
|
+
font-size: $input-font-size-lg;
|
|
129
|
+
@include border-radius($input-border-radius-lg);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.otp-separator {
|
|
133
|
+
font-size: $input-font-size-lg;
|
|
134
|
+
line-height: $input-height-lg;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@keyframes otp-blink {
|
|
140
|
+
0%,
|
|
141
|
+
100% { opacity: 1; }
|
|
142
|
+
50% { opacity: 0; }
|
|
143
|
+
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
@each $state, $data in $form-validation-states {
|
|
2
2
|
$color: map-get($data, color);
|
|
3
|
-
$border-color: $color;
|
|
3
|
+
$border-color: map-get($data, border-color);
|
|
4
|
+
$focus-box-shadow: map-get($data, focus-box-shadow);
|
|
4
5
|
@include form-check-tiled-validation-state($state, $color, $border-color);
|
|
5
6
|
@include form-check-encapsulated-validation-state($state, $color);
|
|
7
|
+
|
|
8
|
+
.otp {
|
|
9
|
+
@include otp-validation-state($state, $border-color, $focus-box-shadow);
|
|
10
|
+
}
|
|
6
11
|
}
|
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
@mixin otp-validation-state($state, $border-color, $focus-box-shadow) {
|
|
2
|
+
// pattern attribute ensures :valid only fires when all N chars match,
|
|
3
|
+
// so native :valid/:invalid + :user-valid/:user-invalid work correctly for both states.
|
|
4
|
+
.was-validated &:has(.otp-input:#{$state}),
|
|
5
|
+
&:has(.otp-input:user-#{$state}),
|
|
6
|
+
&:has(.otp-input.is-#{$state}) {
|
|
7
|
+
.otp-slot {
|
|
8
|
+
border-color: $border-color;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.otp-slot.otp-slot-active {
|
|
12
|
+
border-color: $border-color;
|
|
13
|
+
box-shadow: $focus-box-shadow;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
1
18
|
@mixin form-check-tiled-validation-state($state, $color, $border-color: $color) {
|
|
2
19
|
.form-check.form-check-tiled > .form-check-input {
|
|
3
20
|
@include form-validation-state-selector($state) {
|