@juspay/svelte-ui-components 2.111.3 → 2.112.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.
@@ -0,0 +1,122 @@
1
+ <script lang="ts">
2
+ import Input from '../Input/Input.svelte';
3
+ import Pill from '../Pill/Pill.svelte';
4
+ import type { ChipInputProperties } from './properties';
5
+
6
+ let {
7
+ values = $bindable([]),
8
+ placeholder = 'Add value…',
9
+ disabled = false,
10
+ testId,
11
+ classes,
12
+ onadd,
13
+ ondismiss,
14
+ onchange
15
+ }: ChipInputProperties = $props();
16
+
17
+ let draft = $state('');
18
+
19
+ function addChip(): void {
20
+ const trimmed = draft.trim();
21
+ draft = '';
22
+ if (trimmed.length === 0 || values.includes(trimmed)) {
23
+ return;
24
+ }
25
+ values = [...values, trimmed];
26
+ onadd?.(trimmed);
27
+ onchange?.([...values]);
28
+ }
29
+
30
+ function removeChip(chip: string): void {
31
+ // values is dedup-only by construction (addChip rejects a value already present), so this
32
+ // component's own commit path never produces duplicates. If a caller assigns `values`
33
+ // directly through the bindable prop with a duplicate, remove only the first matching
34
+ // occurrence rather than every occurrence with that text.
35
+ const chipIndex = values.indexOf(chip);
36
+ if (chipIndex === -1) {
37
+ return;
38
+ }
39
+ values = [...values.slice(0, chipIndex), ...values.slice(chipIndex + 1)];
40
+ ondismiss?.(chip);
41
+ onchange?.([...values]);
42
+ }
43
+
44
+ function handleKeyDown(event: KeyboardEvent): void {
45
+ if (event.key === 'Enter') {
46
+ event.preventDefault();
47
+ addChip();
48
+ }
49
+ }
50
+ </script>
51
+
52
+ <div class="chip-input {classes ?? ''}" data-pw={testId} testID={testId}>
53
+ {#each values as chip (chip)}
54
+ <Pill
55
+ text={chip}
56
+ classes="chip-input-pill"
57
+ dismissible={!disabled}
58
+ {disabled}
59
+ ondismiss={() => removeChip(chip)}
60
+ {...typeof testId === 'string' ? { testId: `${testId}-chip` } : {}}
61
+ />
62
+ {/each}
63
+ <div class="chip-input-draft-wrap">
64
+ <Input
65
+ value={draft}
66
+ {placeholder}
67
+ dataType="text"
68
+ name=""
69
+ autoComplete="off"
70
+ actionInput={false}
71
+ disable={disabled}
72
+ classes="chip-input-draft"
73
+ onInput={(nextValue) => {
74
+ draft = nextValue;
75
+ }}
76
+ onKeyDown={handleKeyDown}
77
+ onBlur={addChip}
78
+ {...typeof testId === 'string' ? { testId: `${testId}-input` } : {}}
79
+ />
80
+ </div>
81
+ </div>
82
+
83
+ <style>
84
+ .chip-input {
85
+ display: flex;
86
+ flex-wrap: var(--chip-input-flex-wrap, wrap);
87
+ align-items: var(--chip-input-align-items, center);
88
+ justify-content: var(--chip-input-justify-content, flex-start);
89
+ gap: var(--chip-input-gap, 6px);
90
+ width: var(--chip-input-width, 100%);
91
+ }
92
+
93
+ .chip-input-draft-wrap {
94
+ flex: var(--chip-input-draft-flex, 0 1 auto);
95
+ }
96
+
97
+ .chip-input :global(.chip-input-pill) {
98
+ --pill-gap: var(--chip-input-pill-gap, 4px);
99
+ --pill-background: var(--chip-input-pill-background, #e0e0e0);
100
+ --pill-color: var(--chip-input-pill-color, #333333);
101
+ --pill-font-size: var(--chip-input-pill-font-size, 13px);
102
+ --pill-font-weight: var(--chip-input-pill-font-weight, 500);
103
+ --pill-padding: var(--chip-input-pill-padding, 6px 10px);
104
+ --pill-border-radius: var(--chip-input-pill-border-radius, 999px);
105
+ --pill-border: var(--chip-input-pill-border, none);
106
+ --pill-max-width: var(--chip-input-pill-max-width);
107
+ --pill-dismiss-size: var(--chip-input-pill-dismiss-size, 14px);
108
+ --pill-dismiss-color: var(--chip-input-pill-dismiss-color, currentColor);
109
+ }
110
+
111
+ .chip-input-draft-wrap :global(.chip-input-draft) {
112
+ --input-width: var(--chip-input-draft-width, 90px);
113
+ --input-height: var(--chip-input-draft-height, 28px);
114
+ --input-border: var(--chip-input-draft-border, 1px solid transparent);
115
+ --input-radius: var(--chip-input-draft-radius, 4px);
116
+ --input-focus-border: var(--chip-input-draft-focus-border, 1px solid transparent);
117
+ --input-padding: var(--chip-input-draft-padding, 0 2px);
118
+ --input-margin: 0;
119
+ --input-font-size: var(--chip-input-draft-font-size, 13px);
120
+ --input-box-shadow: none;
121
+ }
122
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { ChipInputProperties } from './properties';
2
+ declare const ChipInput: import("svelte").Component<ChipInputProperties, {}, "values">;
3
+ type ChipInput = ReturnType<typeof ChipInput>;
4
+ export default ChipInput;
@@ -0,0 +1,22 @@
1
+ export type ChipInputProperties = MandatoryChipInputProperties & OptionalChipInputProperties & ChipInputEventProperties;
2
+ export type MandatoryChipInputProperties = {
3
+ /**
4
+ * The committed chips, in insertion order. Bindable. Dedup-only contract: the component's own
5
+ * commit path (`addChip`) silently drops a value already present, so ChipInput never produces
6
+ * duplicates itself. If a caller assigns a duplicate directly through the binding, dismiss
7
+ * removes only the first matching occurrence rather than every occurrence with that text.
8
+ */
9
+ values: string[];
10
+ };
11
+ export type OptionalChipInputProperties = {
12
+ placeholder?: string;
13
+ disabled?: boolean;
14
+ testId?: string;
15
+ classes?: string;
16
+ };
17
+ export type ChipInputEventProperties = {
18
+ onadd?: (value: string) => void;
19
+ ondismiss?: (value: string) => void;
20
+ /** Fires alongside `onadd`/`ondismiss`, after either has already updated `values`. */
21
+ onchange?: (values: string[]) => void;
22
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -10,6 +10,7 @@
10
10
  sandbox,
11
11
  loading,
12
12
  referrerpolicy,
13
+ credentialless,
13
14
  testId,
14
15
  classes,
15
16
  onMessage = () => {}
@@ -17,6 +18,10 @@
17
18
 
18
19
  let iframeEl: HTMLIFrameElement | null = $state(null);
19
20
 
21
+ export const postMessage = (message: unknown, targetOrigin: string): void => {
22
+ iframeEl?.contentWindow?.postMessage(message, targetOrigin);
23
+ };
24
+
20
25
  const messageHandler = (event: MessageEvent): void => {
21
26
  // Secure by default: forward a message only when its origin is allow-listed AND it
22
27
  // originates from the embedded iframe's own window. An empty allowedOrigins list
@@ -44,7 +49,15 @@
44
49
 
45
50
  <div class="iframe-viewer {classes ?? ''}" data-pw={testId} testID={testId}>
46
51
  {#if src}
47
- <iframe bind:this={iframeEl} {src} {title} {allow} {sandbox} {loading} {referrerpolicy}
52
+ <iframe
53
+ bind:this={iframeEl}
54
+ {src}
55
+ {title}
56
+ {allow}
57
+ {sandbox}
58
+ {loading}
59
+ {referrerpolicy}
60
+ {...credentialless ? { credentialless: '' } : {}}
48
61
  ></iframe>
49
62
  {/if}
50
63
  </div>
@@ -1,4 +1,6 @@
1
1
  import type { IframeViewerProperties } from './properties';
2
- declare const IframeViewer: import("svelte").Component<IframeViewerProperties, {}, "">;
2
+ declare const IframeViewer: import("svelte").Component<IframeViewerProperties, {
3
+ postMessage: (message: unknown, targetOrigin: string) => void;
4
+ }, "">;
3
5
  type IframeViewer = ReturnType<typeof IframeViewer>;
4
6
  export default IframeViewer;
@@ -15,6 +15,13 @@ export type OptionalIframeViewerProperties = {
15
15
  allow?: string;
16
16
  /** Value for the iframe `sandbox` attribute. Omitted when not set. */
17
17
  sandbox?: string;
18
+ /**
19
+ * Sets the iframe's `credentialless` attribute, isolating it from the embedding
20
+ * document's credentials/storage. Applied in the same render statement as `src` (not
21
+ * via a post-mount effect), so it is guaranteed to be present before the iframe's
22
+ * first load — no timing dependency on effect scheduling.
23
+ */
24
+ credentialless?: boolean;
18
25
  /** Loading strategy for the iframe. Omitted when not set. */
19
26
  loading?: 'eager' | 'lazy';
20
27
  /** Referrer policy for the iframe. Omitted when not set. */
@@ -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[];
package/dist/index.d.ts CHANGED
@@ -75,6 +75,7 @@ export { default as DeltaIndicator } from './DeltaIndicator/DeltaIndicator.svelt
75
75
  export { default as DualAxisBarChart } from './DualAxisBarChart/DualAxisBarChart.svelte';
76
76
  export { default as FunnelChart } from './FunnelChart/FunnelChart.svelte';
77
77
  export { default as ProportionBar } from './ProportionBar/ProportionBar.svelte';
78
+ export { default as ChipInput } from './ChipInput/ChipInput.svelte';
78
79
  export type * from './Button/properties';
79
80
  export type * from './Modal/properties';
80
81
  export type * from './Input/properties';
@@ -146,6 +147,7 @@ export type * from './DeltaIndicator/properties';
146
147
  export type * from './DualAxisBarChart/properties';
147
148
  export type * from './FunnelChart/properties';
148
149
  export type * from './ProportionBar/properties';
150
+ export type * from './ChipInput/properties';
149
151
  export type * from './_chart/highlight';
150
152
  export { validateInput } from './utils';
151
153
  export { formatNumberIndian } from './_chart/format';
package/dist/index.js CHANGED
@@ -75,5 +75,6 @@ export { default as DeltaIndicator } from './DeltaIndicator/DeltaIndicator.svelt
75
75
  export { default as DualAxisBarChart } from './DualAxisBarChart/DualAxisBarChart.svelte';
76
76
  export { default as FunnelChart } from './FunnelChart/FunnelChart.svelte';
77
77
  export { default as ProportionBar } from './ProportionBar/ProportionBar.svelte';
78
+ export { default as ChipInput } from './ChipInput/ChipInput.svelte';
78
79
  export { validateInput } from './utils';
79
80
  export { formatNumberIndian } from './_chart/format';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.111.3",
3
+ "version": "2.112.0",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",