@juspay/svelte-ui-components 2.123.0 → 2.124.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.
@@ -24,7 +24,9 @@
24
24
  ariaExpanded,
25
25
  ariaHaspopup,
26
26
  ariaSelected,
27
+ ariaBusy,
27
28
  role,
29
+ title,
28
30
  onclick,
29
31
  onkeydown = () => {},
30
32
  onkeyup = () => {},
@@ -91,7 +93,8 @@
91
93
  aria-expanded={ariaExpanded ?? null}
92
94
  aria-haspopup={ariaHaspopup ?? null}
93
95
  aria-selected={ariaSelected ?? null}
94
- aria-busy={isBusy || null}
96
+ aria-busy={isBusy || ariaBusy || null}
97
+ title={title ?? null}
95
98
  type={href ? null : type}
96
99
  disabled={href ? null : isDisabled}
97
100
  href={href ? (isDisabled ? null : href) : null}
@@ -59,7 +59,20 @@ export type OptionalButtonProperties = {
59
59
  */
60
60
  ariaHaspopup?: 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | boolean;
61
61
  ariaSelected?: boolean;
62
+ /**
63
+ * Native `aria-busy`, for a control that stays usable while related data loads.
64
+ * Deliberately separate from `loading`, which also renders the spinner and
65
+ * disables the button — a trigger whose *contents* are still loading must stay
66
+ * clickable, so it cannot express that state through `loading`.
67
+ */
68
+ ariaBusy?: boolean;
62
69
  role?: string;
70
+ /**
71
+ * Native `title`, rendered as the browser's own hover tooltip. Distinct from
72
+ * `ariaLabel`, which names the control for assistive tech without any visible
73
+ * affordance; an icon-only button generally wants both.
74
+ */
75
+ title?: string;
63
76
  disabled?: boolean;
64
77
  classes?: string;
65
78
  /**
@@ -115,6 +115,9 @@
115
115
  const hasRightIcon = $derived(typeof rightIcon === 'function');
116
116
 
117
117
  const charCount = $derived(value?.length ?? 0);
118
+ // Every numeric use below is either tel-only normalisation or the character
119
+ // counter; `null` means "no attribute", not "no ceiling on those paths".
120
+ const effectiveMaxLength = $derived(maxLength ?? 1000);
118
121
  const effectiveResize = $derived(autoResize ? 'none' : resize);
119
122
 
120
123
  // Grow the textarea to fit its content between minRows and maxRows.
@@ -130,8 +133,16 @@
130
133
  const border = parseFloat(styles.borderTopWidth) + parseFloat(styles.borderBottomWidth);
131
134
  const lower = minRows ?? rows ?? 2;
132
135
  const minHeight = lower * lineHeight + verticalPadding + border;
133
- const maxHeight =
136
+ const rowsCeiling =
134
137
  maxRows != null ? maxRows * lineHeight + verticalPadding + border : Number.POSITIVE_INFINITY;
138
+ // --input-max-height is a ceiling too. Reading only maxRows left it at Infinity, so the
139
+ // inline height grew past the CSS clamp and overflowY was set to `hidden` — the box
140
+ // stopped at the right size but its overflow became unreachable instead of scrollable.
141
+ const styleCeiling = parseFloat(styles.maxHeight);
142
+ const maxHeight = Math.min(
143
+ rowsCeiling,
144
+ Number.isFinite(styleCeiling) ? styleCeiling : Number.POSITIVE_INFINITY
145
+ );
135
146
  const nextHeight = Math.min(Math.max(el.scrollHeight, minHeight), maxHeight);
136
147
  el.style.height = `${nextHeight}px`;
137
148
  el.style.overflowY = el.scrollHeight > maxHeight ? 'auto' : 'hidden';
@@ -163,16 +174,16 @@
163
174
  inputElement.value = value;
164
175
  return;
165
176
  }
166
- if (numberLength > maxLength) {
177
+ if (numberLength > effectiveMaxLength) {
167
178
  const existingInput = value;
168
- if (existingInput.length === maxLength) {
179
+ if (existingInput.length === effectiveMaxLength) {
169
180
  inputElement.value = applyTextPresentation(value);
170
181
  return;
171
182
  }
172
183
  /**
173
184
  * choose last max length number of digits if length is bigger than max length passed in props
174
185
  */
175
- currentValue = currentValue.substring(numberLength - maxLength);
186
+ currentValue = currentValue.substring(numberLength - effectiveMaxLength);
176
187
  }
177
188
  currentValue = applyTextPresentation(currentValue);
178
189
  inputElement.value = currentValue;
@@ -221,12 +232,12 @@
221
232
  /**
222
233
  * user pasted 10+ digit number , overrides all cases
223
234
  */
224
- if (filteredNumber.length > maxLength) {
235
+ if (filteredNumber.length > effectiveMaxLength) {
225
236
  /**
226
237
  * choose last max length number of digits if length is bigger than max length passed in props
227
238
  */
228
239
  const finalValue = applyTextPresentation(
229
- filteredNumber.substring(filteredNumberLength - maxLength)
240
+ filteredNumber.substring(filteredNumberLength - effectiveMaxLength)
230
241
  );
231
242
  // Adding reactivity
232
243
  value = finalValue;
@@ -390,8 +401,8 @@
390
401
  </div>
391
402
  {/if}
392
403
  {#if useTextArea && showCount && !actionInput}
393
- <div class="input-char-count" class:at-limit={charCount >= maxLength}>
394
- {charCount}/{maxLength}
404
+ <div class="input-char-count" class:at-limit={charCount >= effectiveMaxLength}>
405
+ {charCount}/{effectiveMaxLength}
395
406
  </div>
396
407
  {/if}
397
408
  </div>
@@ -401,6 +412,13 @@
401
412
  input {
402
413
  box-sizing: var(--input-box-sizing, border-box);
403
414
  height: var(--input-height, fit-content);
415
+
416
+ /* Both default to the CSS initial value, so a consumer that sets neither is
417
+ byte-identical to before. A textarea that grows with its content needs a
418
+ ceiling before it can scroll, and one used as a paste target needs a floor;
419
+ neither was reachable through the --input-* surface. */
420
+ min-height: var(--input-min-height, auto);
421
+ max-height: var(--input-max-height, none);
404
422
  background-color: var(--input-background, white);
405
423
  font-size: var(--input-font-size, 16px) !important;
406
424
  font-family: var(--input-font-family, inherit);
@@ -408,6 +426,12 @@
408
426
  outline: none;
409
427
  padding: var(--input-padding, 16px);
410
428
  font-weight: var(--input-font-weight, 500);
429
+
430
+ /* `normal` is what a textarea/input computes today regardless of any inherited
431
+ value — the UA sheet sets it, and inheritance loses to a UA declaration on the
432
+ element itself. So the default here is byte-identical for existing consumers,
433
+ and this is the only way a consumer can set it at all. */
434
+ line-height: var(--input-line-height, normal);
411
435
  width: var(--input-width, fit-content);
412
436
  margin: var(--input-margin, 0);
413
437
  appearance: none !important;
@@ -29,7 +29,12 @@ export type OptionalInputProperties = {
29
29
  validationPattern?: RegExp | null;
30
30
  inProgressPattern?: RegExp | null;
31
31
  addFocusColor?: boolean;
32
- maxLength?: number;
32
+ /**
33
+ * Native `maxlength`. Defaults to 1000. Pass `null` for no limit — a composer or
34
+ * paste target that silently truncates long input is worse than an unbounded one,
35
+ * and the attribute is rendered unconditionally otherwise.
36
+ */
37
+ maxLength?: number | null;
33
38
  minLength?: number;
34
39
  min?: number;
35
40
  max?: number;
@@ -200,9 +200,28 @@
200
200
  focusedIndex = -1;
201
201
  typeaheadQuery = '';
202
202
  onclose?.();
203
- if (triggerEl !== null) {
204
- triggerEl.focus({ preventScroll: true });
203
+ focusTrigger();
204
+ }
205
+
206
+ /**
207
+ * Returns focus to whatever is actually focusable for this trigger. Under
208
+ * `interactiveTrigger` the wrapper carries no tabindex, so focusing it is a no-op and
209
+ * focus falls to <body> — the control the snippet rendered is the real target.
210
+ */
211
+ function focusTrigger() {
212
+ if (triggerEl === null) {
213
+ return;
205
214
  }
215
+ if (interactiveTrigger) {
216
+ const control = triggerEl.querySelector(
217
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
218
+ );
219
+ if (control instanceof HTMLElement) {
220
+ control.focus({ preventScroll: true });
221
+ return;
222
+ }
223
+ }
224
+ triggerEl.focus({ preventScroll: true });
206
225
  }
207
226
 
208
227
  function selectItem(item: MenuItem) {
@@ -240,6 +259,23 @@
240
259
  }
241
260
  }
242
261
 
262
+ /**
263
+ * Keydown wiring handed to an `interactiveTrigger` snippet. Enter and Space are
264
+ * deliberately NOT handled here: the snippet owns a real `<button>`, which already
265
+ * synthesises a click from both, and that click is already wired to `toggle`. Handling
266
+ * them again would open a menu the click then immediately closes. Only the arrow keys —
267
+ * which no native button implements — are added.
268
+ */
269
+ function handleInteractiveTriggerKeydown(event: KeyboardEvent) {
270
+ if (event.key === 'ArrowDown') {
271
+ event.preventDefault();
272
+ openMenu();
273
+ } else if (event.key === 'ArrowUp') {
274
+ event.preventDefault();
275
+ openMenu(selectableItems.length - 1);
276
+ }
277
+ }
278
+
243
279
  function handleMenuKeydown(event: KeyboardEvent) {
244
280
  switch (event.key) {
245
281
  case 'ArrowDown': {
@@ -358,7 +394,7 @@
358
394
  {#if typeof trigger === 'function'}
359
395
  {@render trigger({
360
396
  onclick: toggle,
361
- onkeydown: handleTriggerKeydown,
397
+ onkeydown: handleInteractiveTriggerKeydown,
362
398
  ariaHaspopup: 'menu',
363
399
  ariaExpanded: open
364
400
  })}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.123.0",
3
+ "version": "2.124.0",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",