@juspay/svelte-ui-components 2.55.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.
- package/dist/Input/Input.svelte +178 -55
- package/dist/Input/properties.d.ts +17 -0
- package/dist/Select/Select.svelte +19 -1
- package/dist/Select/properties.d.ts +4 -0
- package/package.json +1 -1
package/dist/Input/Input.svelte
CHANGED
|
@@ -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={
|
|
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
|
-
{#
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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 !== '' &&
|
|
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;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { onMount, tick } from 'svelte';
|
|
3
3
|
import type { SelectItem, SelectProperties } from './properties';
|
|
4
4
|
import Pill from '../Pill/Pill.svelte';
|
|
5
|
+
import Img from '../Img/Img.svelte';
|
|
5
6
|
import chevronDownSvg from '../assets/chevron-down.svg?raw';
|
|
6
7
|
|
|
7
8
|
let {
|
|
@@ -22,7 +23,9 @@
|
|
|
22
23
|
classes,
|
|
23
24
|
open = $bindable(false),
|
|
24
25
|
dropdownAlign = 'left',
|
|
25
|
-
hierarchy = 'default'
|
|
26
|
+
hierarchy = 'default',
|
|
27
|
+
leftIcon,
|
|
28
|
+
leftIconTestId
|
|
26
29
|
}: SelectProperties = $props();
|
|
27
30
|
|
|
28
31
|
function normalizeItems(source: SelectItem[] | string[]): SelectItem[] {
|
|
@@ -266,6 +269,14 @@
|
|
|
266
269
|
{...highlightedOptionId !== null ? { 'aria-activedescendant': highlightedOptionId } : {}}
|
|
267
270
|
tabindex={disabled ? -1 : searchable ? -1 : 0}
|
|
268
271
|
>
|
|
272
|
+
{#if typeof leftIcon === 'string' && leftIcon.length > 0}
|
|
273
|
+
<Img
|
|
274
|
+
src={leftIcon}
|
|
275
|
+
alt=""
|
|
276
|
+
classes="select-left-icon"
|
|
277
|
+
{...typeof leftIconTestId === 'string' ? { testId: leftIconTestId } : {}}
|
|
278
|
+
/>
|
|
279
|
+
{/if}
|
|
269
280
|
{#if multiple}
|
|
270
281
|
{#if typeof triggerSummary === 'function'}
|
|
271
282
|
{@render triggerSummary({ value, items })}
|
|
@@ -416,6 +427,13 @@
|
|
|
416
427
|
transition: var(--select-trigger-transition, border-color 0.15s, box-shadow 0.15s);
|
|
417
428
|
}
|
|
418
429
|
|
|
430
|
+
.select-trigger :global(.select-left-icon) {
|
|
431
|
+
--image-width: var(--select-left-icon-size, 16px);
|
|
432
|
+
--image-height: var(--select-left-icon-size, 16px);
|
|
433
|
+
--image-object-fit: contain;
|
|
434
|
+
flex: none;
|
|
435
|
+
}
|
|
436
|
+
|
|
419
437
|
.select-trigger:hover:not(.disabled .select-trigger) {
|
|
420
438
|
border-color: var(--select-trigger-hover-border-color, #999999);
|
|
421
439
|
}
|
|
@@ -35,6 +35,10 @@ export type OptionalSelectProperties = {
|
|
|
35
35
|
}]>;
|
|
36
36
|
/** Visual hierarchy of the trigger. `'ghost'` renders a transparent, borderless trigger — useful when the Select is embedded in a toolbar or header where a full bordered input would be visually heavy. Defaults to `'default'`. */
|
|
37
37
|
hierarchy?: SelectHierarchy;
|
|
38
|
+
/** Optional image src (URL or data URI) rendered at the left of the trigger. Size is controlled by the `--select-left-icon-size` CSS variable (default 16px). */
|
|
39
|
+
leftIcon?: string;
|
|
40
|
+
/** `data-pw` test id forwarded to the leading icon `<Img>` element. */
|
|
41
|
+
leftIconTestId?: string;
|
|
38
42
|
};
|
|
39
43
|
export type SelectEventProperties = {
|
|
40
44
|
onchange?: (value: string[]) => void;
|