@juspay/svelte-ui-components 2.111.2 → 2.111.4

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.
@@ -56,6 +56,32 @@
56
56
 
57
57
  // ── Input handler ───────────────────────────────────────────────
58
58
 
59
+ // Multi-char cleanup for the distribute paths: tel fields keep digits only
60
+ // (an OTP autofilled/pasted as "123-456" or "123 456" must land as 123456,
61
+ // never distribute a separator into a field); other dataTypes keep the old
62
+ // whitespace-strip behavior.
63
+ function sanitizeChars(config: FieldConfig, raw: string): string {
64
+ if ((config.dataType ?? 'text') === 'tel') {
65
+ return raw.replace(/\D/g, '');
66
+ }
67
+ return raw.replace(/\s/g, '');
68
+ }
69
+
70
+ // Single-char autoAdvance fields widen the inner Input's maxLength to the
71
+ // whole code length: Input's dataType='tel' sanitizer truncates an overflowing
72
+ // value to its LAST maxLength digits BEFORE onInput fires, so with the literal
73
+ // maxLength of 1 a WebOTP / Android-SMS autofill that drops the whole code
74
+ // into one field reached handleFieldInput as a single wrong digit and the
75
+ // distribute branch below could never run. One-char-per-field semantics are
76
+ // enforced by handleFieldInput itself, not by the inner Input.
77
+ function innerMaxLength(config: FieldConfig): number {
78
+ const configured = config.maxLength ?? 1000;
79
+ if (autoAdvance && configured === 1) {
80
+ return fieldCount;
81
+ }
82
+ return configured;
83
+ }
84
+
59
85
  function handleFieldInput(index: number, inputValue: string) {
60
86
  const config = fieldConfigs.at(index);
61
87
  if (typeof config === 'undefined') {
@@ -64,11 +90,25 @@
64
90
  const maxLen = config.maxLength ?? 1000;
65
91
 
66
92
  if (autoAdvance && maxLen === 1 && inputValue.length > 1) {
67
- const chars = inputValue.replace(/\s/g, '');
68
- for (let i = 0; i < fieldCount; i++) {
69
- values[i] = chars.charAt(i);
93
+ const chars = sanitizeChars(config, inputValue);
94
+ if (chars.length === 0) {
95
+ values[index] = '';
96
+ } else if (index === 0 || chars.length >= fieldCount) {
97
+ // Autofill / OS-level insertion of a whole code: distribute from the
98
+ // start. Only provided characters are written — a partial string must
99
+ // not clear fields beyond it.
100
+ for (let i = 0; i < Math.min(chars.length, fieldCount); i++) {
101
+ values[i] = chars.charAt(i);
102
+ }
103
+ focusField(Math.min(chars.length, fieldCount) - 1);
104
+ } else {
105
+ // Overtyping an already-filled field: keep the newest character and
106
+ // advance, mirroring single-char entry.
107
+ values[index] = chars.slice(-1);
108
+ if (index < fieldCount - 1) {
109
+ focusField(index + 1);
110
+ }
70
111
  }
71
- focusField(Math.min(chars.length, fieldCount) - 1);
72
112
  } else {
73
113
  values[index] = inputValue;
74
114
  if (autoAdvance && maxLen === 1 && inputValue.length > 0 && index < fieldCount - 1) {
@@ -111,9 +151,10 @@
111
151
  }
112
152
  e.preventDefault();
113
153
  const pasted = e.clipboardData.getData('text').trim();
154
+ const config = fieldConfigs.at(index);
114
155
 
115
- if (autoAdvance) {
116
- const chars = pasted.replace(/\s/g, '');
156
+ if (autoAdvance && typeof config !== 'undefined') {
157
+ const chars = sanitizeChars(config, pasted);
117
158
  for (let i = index; i < fieldCount; i++) {
118
159
  const charIdx = i - index;
119
160
  if (charIdx >= chars.length) {
@@ -153,7 +194,8 @@
153
194
  <Input
154
195
  value={getFieldValue(index)}
155
196
  dataType={config.dataType ?? 'text'}
156
- maxLength={config.maxLength ?? 1000}
197
+ maxLength={innerMaxLength(config)}
198
+ testId={config.testId}
157
199
  min={config.min}
158
200
  max={config.max}
159
201
  placeholder={config.placeholder}
@@ -1,5 +1,5 @@
1
1
  import type { OptionalInputProperties } from '../Input/properties';
2
- export type FieldConfig = Pick<OptionalInputProperties, 'dataType' | 'maxLength' | 'min' | 'max' | 'placeholder' | 'validationPattern' | 'validators' | 'label' | 'autoComplete' | 'inputMode'>;
2
+ export type FieldConfig = Pick<OptionalInputProperties, 'dataType' | 'maxLength' | 'min' | 'max' | 'placeholder' | 'validationPattern' | 'validators' | 'label' | 'autoComplete' | 'inputMode' | 'testId'>;
3
3
  export type SplitInputProperties = MandatorySplitInputProperties & OptionalSplitInputProperties & SplitInputEventProperties;
4
4
  export type MandatorySplitInputProperties = {
5
5
  values: string[];
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { tick } from 'svelte';
2
3
  import type { TabItem, TabsProperties } from './properties';
3
4
  import Img from '../Img/Img.svelte';
4
5
  import chevronLeftSvg from '../assets/chevron-left.svg?raw';
@@ -149,11 +150,49 @@
149
150
  }
150
151
  }
151
152
 
153
+ // After a keyboard-driven selection the active item changes (directly in
154
+ // string mode; via the consumer's activeKey update in object mode), and the
155
+ // roving tabindex re-roves with it. Move DOM focus onto the newly active tab
156
+ // so the user can keep arrowing — without this, focus is stranded on a
157
+ // tabindex="-1" item and the roving pattern breaks down.
158
+ function moveFocusToActiveTab(): void {
159
+ void tick().then(() => {
160
+ const activeTab = scrollContainer?.querySelector<HTMLElement>('[role="tab"][tabindex="0"]');
161
+ activeTab?.focus();
162
+ });
163
+ }
164
+
165
+ // WAI-ARIA APG tablist keyboard contract with activation-follows-focus:
166
+ // Arrow keys (orientation-aware) move selection to the adjacent tab with
167
+ // wrap-around, Home/End jump to the first/last tab. Without this, the roving
168
+ // tabindex makes every non-active tab keyboard-unreachable.
152
169
  function handleKeydown(event: KeyboardEvent, index: number): void {
153
170
  if (event.key === 'Enter' || event.key === ' ') {
154
171
  event.preventDefault();
155
172
  handleTabClick(index);
173
+ return;
174
+ }
175
+ const nextKey = isVertical ? 'ArrowDown' : 'ArrowRight';
176
+ const previousKey = isVertical ? 'ArrowUp' : 'ArrowLeft';
177
+ let targetIndex: number | null = null;
178
+ if (event.key === nextKey) {
179
+ targetIndex = (index + 1) % items.length;
180
+ } else if (event.key === previousKey) {
181
+ targetIndex = (index - 1 + items.length) % items.length;
182
+ } else if (event.key === 'Home') {
183
+ targetIndex = 0;
184
+ } else if (event.key === 'End') {
185
+ targetIndex = items.length - 1;
186
+ }
187
+ if (targetIndex === null) {
188
+ return;
189
+ }
190
+ event.preventDefault();
191
+ if (disabled || targetIndex === index) {
192
+ return;
156
193
  }
194
+ handleTabClick(targetIndex);
195
+ moveFocusToActiveTab();
157
196
  }
158
197
 
159
198
  function initOverflow(node: HTMLDivElement): () => void {
@@ -209,6 +248,7 @@
209
248
  class:fade-top={canScrollUp}
210
249
  class:fade-bottom={canScrollDown}
211
250
  role="tablist"
251
+ aria-orientation={isVertical ? 'vertical' : null}
212
252
  {@attach initOverflow}
213
253
  onscroll={updateOverflow}
214
254
  >
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.111.2",
3
+ "version": "2.111.4",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",