@juspay/svelte-ui-components 2.56.0 → 2.57.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.
@@ -39,7 +39,15 @@
39
39
  ariaExpanded,
40
40
  ariaAutocomplete,
41
41
  ariaControls,
42
- ariaActivedescendant
42
+ ariaActivedescendant,
43
+ leftIcon,
44
+ rightIcon,
45
+ onLeftIconClick,
46
+ onRightIconClick,
47
+ leftIconLabel = 'Leading action',
48
+ rightIconLabel = 'Trailing action',
49
+ mandatory = false,
50
+ forceError = false
43
51
  }: InputProperties = $props();
44
52
 
45
53
  export function focus() {
@@ -90,6 +98,11 @@
90
98
  });
91
99
 
92
100
  const showErrorMessage = $derived(validationState === 'Invalid');
101
+ // forceError lets consumers drive the error border from server/runtime validation,
102
+ // independent of validationPattern.
103
+ const showError = $derived(showErrorMessage || forceError);
104
+ const hasLeftIcon = $derived(typeof leftIcon === 'function');
105
+ const hasRightIcon = $derived(typeof rightIcon === 'function');
93
106
 
94
107
  function handleOnInput(event: Event) {
95
108
  if (inputElement === null) {
@@ -192,68 +205,113 @@
192
205
  }
193
206
  </script>
194
207
 
195
- <div class="input-container {classes ?? ''}" class:input-error={showErrorMessage && !actionInput}>
208
+ <div class="input-container {classes ?? ''}" class:input-error={showError && !actionInput}>
196
209
  {#if typeof label === 'string' && label !== '' && !actionInput}
197
210
  <label class="label" for={name}>
198
- {label}
211
+ {label}{#if mandatory}<span class="input-mandatory-asterisk" aria-hidden="true">*</span>{/if}
199
212
  </label>
200
213
  {/if}
201
214
 
202
- {#if useTextArea}
203
- <!-- svelte-ignore element_invalid_self_closing_tag -->
204
- <textarea
205
- {value}
206
- {placeholder}
207
- autocomplete={autoComplete}
208
- {name}
209
- {role}
210
- aria-expanded={ariaExpanded}
211
- aria-autocomplete={ariaAutocomplete}
212
- aria-controls={ariaControls}
213
- aria-activedescendant={ariaActivedescendant}
214
- onfocus={onFocus}
215
- onfocusout={_onFocusOut}
216
- oninput={handleOnInput}
217
- onpaste={handleOnPaste}
218
- onclick={onClick}
219
- onkeydown={onKeyDown}
220
- class:action-input={actionInput}
221
- style="--focus-border: {addFocusColor ? 1 : 0}px;"
222
- disabled={disable}
223
- bind:this={inputElement}
224
- maxlength={dataType === 'tel' ? null : maxLength}
225
- minlength={minLength}
226
- />
215
+ {#snippet fieldElement()}
216
+ {#if useTextArea}
217
+ <textarea
218
+ {value}
219
+ {placeholder}
220
+ autocomplete={autoComplete}
221
+ {name}
222
+ {role}
223
+ aria-expanded={ariaExpanded}
224
+ aria-autocomplete={ariaAutocomplete}
225
+ aria-controls={ariaControls}
226
+ aria-activedescendant={ariaActivedescendant}
227
+ aria-required={mandatory || null}
228
+ required={mandatory || null}
229
+ onfocus={onFocus}
230
+ onfocusout={_onFocusOut}
231
+ oninput={handleOnInput}
232
+ onpaste={handleOnPaste}
233
+ onclick={onClick}
234
+ onkeydown={onKeyDown}
235
+ class:action-input={actionInput}
236
+ style="--focus-border: {addFocusColor ? 1 : 0}px;"
237
+ disabled={disable}
238
+ bind:this={inputElement}
239
+ maxlength={dataType === 'tel' ? null : maxLength}
240
+ minlength={minLength}
241
+ ></textarea>
242
+ {:else}
243
+ <input
244
+ type={dataType}
245
+ {value}
246
+ {placeholder}
247
+ autocomplete={autoComplete}
248
+ {name}
249
+ {role}
250
+ aria-expanded={ariaExpanded}
251
+ aria-autocomplete={ariaAutocomplete}
252
+ aria-controls={ariaControls}
253
+ aria-activedescendant={ariaActivedescendant}
254
+ aria-required={mandatory || null}
255
+ required={mandatory || null}
256
+ onfocus={onFocus}
257
+ onfocusout={_onFocusOut}
258
+ oninput={handleOnInput}
259
+ onpaste={handleOnPaste}
260
+ onclick={onClick}
261
+ onkeydown={onKeyDown}
262
+ data-pw={testId}
263
+ class:action-input={actionInput}
264
+ disabled={disable}
265
+ bind:this={inputElement}
266
+ maxlength={dataType === 'tel' ? null : maxLength}
267
+ minlength={minLength}
268
+ {min}
269
+ {max}
270
+ />
271
+ {/if}
272
+ {/snippet}
273
+
274
+ {#if hasLeftIcon || hasRightIcon}
275
+ <div
276
+ class="input-field-wrap"
277
+ class:has-left-icon={hasLeftIcon}
278
+ class:has-right-icon={hasRightIcon}
279
+ >
280
+ {#if hasLeftIcon}
281
+ {#if onLeftIconClick}
282
+ <button
283
+ type="button"
284
+ class="input-icon input-icon-left input-icon-button"
285
+ aria-label={leftIconLabel}
286
+ onclick={onLeftIconClick}
287
+ >
288
+ {@render leftIcon?.()}
289
+ </button>
290
+ {:else}
291
+ <span class="input-icon input-icon-left">{@render leftIcon?.()}</span>
292
+ {/if}
293
+ {/if}
294
+ {@render fieldElement()}
295
+ {#if hasRightIcon}
296
+ {#if onRightIconClick}
297
+ <button
298
+ type="button"
299
+ class="input-icon input-icon-right input-icon-button"
300
+ aria-label={rightIconLabel}
301
+ onclick={onRightIconClick}
302
+ >
303
+ {@render rightIcon?.()}
304
+ </button>
305
+ {:else}
306
+ <span class="input-icon input-icon-right">{@render rightIcon?.()}</span>
307
+ {/if}
308
+ {/if}
309
+ </div>
227
310
  {:else}
228
- <input
229
- type={dataType}
230
- {value}
231
- {placeholder}
232
- autocomplete={autoComplete}
233
- {name}
234
- {role}
235
- aria-expanded={ariaExpanded}
236
- aria-autocomplete={ariaAutocomplete}
237
- aria-controls={ariaControls}
238
- aria-activedescendant={ariaActivedescendant}
239
- onfocus={onFocus}
240
- onfocusout={_onFocusOut}
241
- oninput={handleOnInput}
242
- onpaste={handleOnPaste}
243
- onclick={onClick}
244
- onkeydown={onKeyDown}
245
- data-pw={testId}
246
- class:action-input={actionInput}
247
- disabled={disable}
248
- bind:this={inputElement}
249
- maxlength={dataType === 'tel' ? null : maxLength}
250
- minlength={minLength}
251
- {min}
252
- {max}
253
- />
311
+ {@render fieldElement()}
254
312
  {/if}
255
313
 
256
- {#if onErrorMessage !== '' && showErrorMessage && !actionInput}
314
+ {#if onErrorMessage !== '' && showError && !actionInput}
257
315
  <div class="error-message">
258
316
  {onErrorMessage}
259
317
  </div>
@@ -327,6 +385,71 @@
327
385
  padding: var(--input-label-msg-padding);
328
386
  }
329
387
 
388
+ .input-mandatory-asterisk {
389
+ color: var(--input-mandatory-color, var(--input-error-msg-text-color, #fa1405));
390
+ margin-left: var(--input-mandatory-gap, 2px);
391
+ }
392
+
393
+ /* Icon wrapper: only rendered when leftIcon/rightIcon is supplied, so non-icon
394
+ consumers keep the exact prior DOM. The field's bottom margin moves to the
395
+ wrap so the absolutely-positioned icons centre on the field, not the margin. */
396
+ .input-field-wrap {
397
+ position: relative;
398
+ display: block;
399
+ margin: var(--input-margin, 0px 0px 12px 0px);
400
+ }
401
+
402
+ .input-field-wrap > :global(textarea),
403
+ .input-field-wrap > :global(input) {
404
+ margin: 0 !important;
405
+ width: var(--input-width, 100%);
406
+ }
407
+
408
+ .input-icon {
409
+ position: absolute;
410
+ top: 50%;
411
+ transform: translateY(-50%);
412
+ display: inline-flex;
413
+ align-items: center;
414
+ justify-content: center;
415
+ width: var(--input-icon-size, 20px);
416
+ height: var(--input-icon-size, 20px);
417
+ color: var(--input-icon-color, inherit);
418
+ pointer-events: none;
419
+ }
420
+
421
+ .input-icon-button {
422
+ background: none;
423
+ border: none;
424
+ padding: 0;
425
+ cursor: pointer;
426
+ pointer-events: auto;
427
+ }
428
+
429
+ .input-icon-button:focus-visible {
430
+ outline: var(--input-icon-focus-outline, 2px solid var(--input-focus-border-color, #005fcc));
431
+ outline-offset: var(--input-icon-focus-outline-offset, 2px);
432
+ border-radius: var(--input-icon-focus-radius, 2px);
433
+ }
434
+
435
+ .input-icon-left {
436
+ left: var(--input-icon-gap, 12px);
437
+ }
438
+
439
+ .input-icon-right {
440
+ right: var(--input-icon-gap, 12px);
441
+ }
442
+
443
+ .input-field-wrap.has-left-icon > :global(textarea),
444
+ .input-field-wrap.has-left-icon > :global(input) {
445
+ padding-left: calc(var(--input-icon-size, 20px) + var(--input-icon-gap, 12px) * 2);
446
+ }
447
+
448
+ .input-field-wrap.has-right-icon > :global(textarea),
449
+ .input-field-wrap.has-right-icon > :global(input) {
450
+ padding-right: calc(var(--input-icon-size, 20px) + var(--input-icon-gap, 12px) * 2);
451
+ }
452
+
330
453
  .error-message {
331
454
  font-weight: var(--input-error-msg-text-weight, 400);
332
455
  font-size: var(--input-error-msg-text-size, 12px);
@@ -1,5 +1,6 @@
1
1
  import type { CustomValidator, InputDataType, TextTransformer, ValidationState } from '../types';
2
2
  import type { HTMLInputAttributes } from 'svelte/elements';
3
+ import type { Snippet } from 'svelte';
3
4
  export type InputProperties = OptionalInputProperties & InputEventProperties & MandatoryInputProperties;
4
5
  export type MandatoryInputProperties = {
5
6
  value: string;
@@ -32,6 +33,22 @@ export type OptionalInputProperties = {
32
33
  ariaAutocomplete?: 'none' | 'inline' | 'list' | 'both';
33
34
  ariaControls?: string | null;
34
35
  ariaActivedescendant?: string | null;
36
+ /** Passive/clickable icon rendered inside the field on the leading edge (e.g. a search icon). */
37
+ leftIcon?: Snippet;
38
+ /** Passive/clickable icon rendered inside the field on the trailing edge (e.g. a clear button). */
39
+ rightIcon?: Snippet;
40
+ /** When set, the leftIcon becomes a focusable button invoking this handler. */
41
+ onLeftIconClick?: () => void;
42
+ /** When set, the rightIcon becomes a focusable button invoking this handler. */
43
+ onRightIconClick?: () => void;
44
+ /** Accessible label for the clickable leftIcon button (defaults to a generic label). */
45
+ leftIconLabel?: string;
46
+ /** Accessible label for the clickable rightIcon button (defaults to a generic label). */
47
+ rightIconLabel?: string;
48
+ /** Appends a required asterisk beside the label and sets aria-required on the field. */
49
+ mandatory?: boolean;
50
+ /** Forces the error border independent of validationPattern (server/runtime-driven errors). */
51
+ forceError?: boolean;
35
52
  };
36
53
  export type InputEventProperties = {
37
54
  onInput?: (value: string, event: Event) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.56.0",
3
+ "version": "2.57.0",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",