@juspay/svelte-ui-components 2.51.0 → 2.53.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.
@@ -3,7 +3,6 @@
3
3
  import Input from '../Input/Input.svelte';
4
4
  import type { InputButtonProperties } from './properties';
5
5
  import type { ValidationState } from '../types';
6
- import type { SvelteComponent } from 'svelte';
7
6
 
8
7
  let {
9
8
  value = $bindable(''),
@@ -17,12 +16,15 @@
17
16
  bottomButtonEventProperties,
18
17
  leftIcon,
19
18
  rightIcon,
20
- classes
19
+ classes,
20
+ mandatory,
21
+ size,
22
+ error
21
23
  }: InputButtonProperties = $props();
22
24
 
23
25
  let validationState = $state<ValidationState>('InProgress');
24
26
 
25
- let inputRef: SvelteComponent | null = $state(null);
27
+ let inputRef: ReturnType<typeof Input> | null = $state(null);
26
28
 
27
29
  // Derive enable state for right button
28
30
  const isRightButtonEnabled = $derived(validationState === 'Valid');
@@ -62,12 +64,14 @@
62
64
  <div class="container {classes ?? ''}">
63
65
  {#if inputProperties.label && inputProperties.label !== ''}
64
66
  <label class="label" for={inputProperties.name}>
65
- {inputProperties.label}
67
+ {inputProperties.label}{#if mandatory}<span class="mandatory-marker" aria-label="required">
68
+ *</span
69
+ >{/if}
66
70
  </label>
67
71
  {/if}
68
72
 
69
- <div class="input-button-container">
70
- <div class="input-button {validationState === 'Invalid' ? 'invalid' : 'valid'}">
73
+ <div class="input-button-container {size ? `size-${size}` : ''}">
74
+ <div class="input-button" class:invalid={validationState === 'Invalid'}>
71
75
  {#if leftButtonProperties != null}
72
76
  <div class="left-button">
73
77
  <Button {...leftButtonProperties} {...leftButtonEventProperties} icon={leftIcon} />
@@ -101,12 +105,14 @@
101
105
  </div>
102
106
  {/if}
103
107
  </div>
104
- {#if inputProperties.onErrorMessage !== '' && validationState === 'Invalid'}
108
+ {#if typeof error === 'string' && error.length > 0}
109
+ <div class="external-error-message">{error}</div>
110
+ {:else if typeof inputProperties.onErrorMessage === 'string' && inputProperties.onErrorMessage !== '' && validationState === 'Invalid'}
105
111
  <div class="error-message">
106
112
  {inputProperties.onErrorMessage}
107
113
  </div>
108
114
  {/if}
109
- {#if inputProperties.infoMessage !== ''}
115
+ {#if typeof inputProperties.infoMessage === 'string' && inputProperties.infoMessage !== ''}
110
116
  <div class="info-message">
111
117
  {inputProperties.infoMessage}
112
118
  </div>
@@ -127,9 +133,8 @@
127
133
  --input-box-shadow: none;
128
134
  --input-margin: none;
129
135
  --input-width: fit-content;
130
- height: var(--input-height, fit-content);
131
136
  font-size: var(--input-font-size, 16px) !important;
132
- font-weight: 500;
137
+ font-weight: var(--input-button-font-weight, 500);
133
138
  margin: var(--input-button-margin);
134
139
  border-radius: var(--input-button-radius, 4px);
135
140
  border: var(--input-button-container-border);
@@ -193,6 +198,30 @@
193
198
  margin: var(--input-btn-info-msg-margin, 12px 0px 0px 0px);
194
199
  }
195
200
 
201
+ .mandatory-marker {
202
+ color: var(--inputbutton-mandatory-color, #e11900);
203
+ }
204
+
205
+ .size-sm {
206
+ --input-height: var(--inputbutton-sm-height, 36px);
207
+ --input-padding: var(--inputbutton-sm-padding, 6px 12px);
208
+ }
209
+
210
+ .size-md {
211
+ --input-height: var(--inputbutton-md-height, 44px);
212
+ --input-padding: var(--inputbutton-md-padding, 10px 16px);
213
+ }
214
+
215
+ .size-lg {
216
+ --input-height: var(--inputbutton-lg-height, 54px);
217
+ --input-padding: var(--inputbutton-lg-padding, 14px 20px);
218
+ }
219
+
220
+ .external-error-message {
221
+ color: var(--inputbutton-external-error-color, #fa1405);
222
+ margin: var(--inputbutton-external-error-margin, 6px 0px 0px 0px);
223
+ }
224
+
196
225
  .left-button {
197
226
  --button-color: var(--left-button-color);
198
227
  --button-text-color: var(--left-button-text-color);
@@ -1,6 +1,16 @@
1
1
  import type { ButtonEventProperties, OptionalButtonProperties } from '../Button/properties';
2
2
  import type { InputEventProperties, OptionalInputProperties } from '../Input/properties';
3
3
  import type { Snippet } from 'svelte';
4
+ /**
5
+ * Preset size variants for the InputButton component.
6
+ * - `sm`: 36px height with compact padding (6px 12px)
7
+ * - `md`: 44px height with standard padding (10px 16px)
8
+ * - `lg`: 54px height with generous padding (14px 20px)
9
+ *
10
+ * All sizes are overridable via the corresponding CSS variables
11
+ * (`--inputbutton-sm-height`, `--inputbutton-md-padding`, etc.).
12
+ */
13
+ export type InputButtonSize = 'sm' | 'md' | 'lg';
4
14
  export type InputButtonProperties = OptionalInputButtonProperties & InputButtonEventProperties & {
5
15
  value: string;
6
16
  };
@@ -12,6 +22,17 @@ export type OptionalInputButtonProperties = {
12
22
  leftIcon?: Snippet;
13
23
  rightIcon?: Snippet;
14
24
  classes?: string;
25
+ /** When true, renders a red asterisk (*) next to the label to signal the field is required. */
26
+ mandatory?: boolean;
27
+ /** Preset height/padding size variant. Overridable per-size via CSS variables. */
28
+ size?: InputButtonSize;
29
+ /**
30
+ * External error message to display below the input-button row. Takes precedence over
31
+ * the internal `inputProperties.onErrorMessage` — when this prop is a non-empty string,
32
+ * the internal validation error is suppressed. Pass an empty string or omit to let
33
+ * internal validation messages surface normally.
34
+ */
35
+ error?: string;
15
36
  };
16
37
  export type InputButtonEventProperties = {
17
38
  inputEventProperties?: InputEventProperties;
@@ -7,7 +7,7 @@
7
7
  import Button from '../Button/Button.svelte';
8
8
 
9
9
  let overlayDiv: HTMLDivElement | null = $state(null);
10
- let backPressed = false;
10
+ let backPressed = $state(false);
11
11
 
12
12
  let {
13
13
  size = 'fit-content',
@@ -30,47 +30,87 @@
30
30
  onsecondaryButtonClick,
31
31
  onoverlayClick,
32
32
  onkeydown,
33
- classes
33
+ classes,
34
+ overlayBackdropFilter,
35
+ usePortal = false
34
36
  }: ModalProperties = $props();
35
37
 
38
+ // Fix [major]: plain const so the debouncer closure retains its internal lastCallTime state
39
+ // across re-renders. $derived would recreate the debouncer on every reactive re-evaluation,
40
+ // resetting the timer and breaking debounce correctness.
36
41
  const debounce = createDebouncer(debounceTime);
37
42
 
38
- function handlePopstate() {
43
+ // Fix [minor]: portalAction with null guard + update hook so usePortal toggles work post-mount.
44
+ const portalAction = (node: HTMLElement, params: { usePortal: boolean }) => {
45
+ if (!params.usePortal || typeof document === 'undefined' || !document.body) {
46
+ return;
47
+ }
48
+ const target = document.body;
49
+ target.appendChild(node);
50
+ return {
51
+ update(updatedParams: { usePortal: boolean }) {
52
+ if (updatedParams.usePortal && !node.parentElement?.isSameNode(document.body)) {
53
+ document.body.appendChild(node);
54
+ }
55
+ },
56
+ destroy() {
57
+ node.parentNode?.removeChild(node);
58
+ }
59
+ };
60
+ };
61
+
62
+ const handlePopstate = (): void => {
39
63
  backPressed = true;
40
64
  onclose?.();
41
- }
65
+ };
42
66
 
43
- function handleRightImageClick(event: MouseEvent): void {
67
+ const handleRightImageClick = (event: MouseEvent): void => {
44
68
  onheaderRightImageClick?.(event);
45
- }
69
+ };
46
70
 
47
- function handleLeftImageClick(event: MouseEvent): void {
71
+ const handleLeftImageClick = (event: MouseEvent): void => {
48
72
  onheaderLeftImageClick?.(event);
49
- }
73
+ };
50
74
 
51
- function handlePrimaryButtonClick(event: MouseEvent): void {
75
+ const handlePrimaryButtonClick = (event: MouseEvent): void => {
52
76
  onprimaryButtonClick?.(event);
53
- }
77
+ };
54
78
 
55
- function handleSecondaryButtonClick(event: MouseEvent): void {
79
+ const handleSecondaryButtonClick = (event: MouseEvent): void => {
56
80
  onsecondaryButtonClick?.(event);
57
- }
81
+ };
58
82
 
59
- function handleOverlayClick(event: MouseEvent) {
83
+ const handleOverlayClick = (event: MouseEvent): void => {
60
84
  if (event.target === overlayDiv) {
61
85
  debounce(() => {
62
86
  onoverlayClick?.();
63
87
  });
64
88
  }
65
- }
89
+ };
66
90
 
67
- function handleKeyDown(event: KeyboardEvent): void {
91
+ const handleKeyDown = (event: KeyboardEvent): void => {
68
92
  onkeydown?.(event);
69
- let key = event?.key;
93
+ const key = event?.key;
70
94
  if (key === 'Escape') {
71
95
  onoverlayClick?.();
72
96
  }
73
- }
97
+ };
98
+
99
+ // Fix [major]: role=button image divs need Enter/Space handlers for WCAG 2.1 SC 2.1.1.
100
+ // Keyboard activation (Enter/Space) invokes the same prop callbacks as click.
101
+ const handleLeftImageKeyDown = (event: KeyboardEvent): void => {
102
+ if (event.key === 'Enter' || event.key === ' ') {
103
+ event.preventDefault();
104
+ onheaderLeftImageClick?.(new MouseEvent('click'));
105
+ }
106
+ };
107
+
108
+ const handleRightImageKeyDown = (event: KeyboardEvent): void => {
109
+ if (event.key === 'Enter' || event.key === ' ') {
110
+ event.preventDefault();
111
+ onheaderRightImageClick?.(new MouseEvent('click'));
112
+ }
113
+ };
74
114
 
75
115
  onMount(() => {
76
116
  document.body.style.overflow = 'hidden';
@@ -99,9 +139,13 @@
99
139
  <OverlayAnimation>
100
140
  <div
101
141
  bind:this={overlayDiv}
142
+ use:portalAction={{ usePortal }}
102
143
  class="modal {align} {showOverlay ? 'overlay-active' : 'overlay-inactive'} {classes ?? ''}"
144
+ style={overlayBackdropFilter != null
145
+ ? `--modal-overlay-backdrop-filter: ${overlayBackdropFilter};`
146
+ : null}
103
147
  onclick={handleOverlayClick}
104
- {onkeydown}
148
+ onkeydown={handleKeyDown}
105
149
  role="button"
106
150
  tabindex="0"
107
151
  data-pw={testId}
@@ -113,7 +157,7 @@
113
157
  {#if typeof header.leftImage === 'string' && header.leftImage.length > 0}
114
158
  <div
115
159
  onclick={handleLeftImageClick}
116
- {onkeydown}
160
+ onkeydown={handleLeftImageKeyDown}
117
161
  role="button"
118
162
  tabindex="0"
119
163
  data-pw={leftImageTestId}
@@ -131,7 +175,7 @@
131
175
  role="button"
132
176
  tabindex="0"
133
177
  onclick={handleRightImageClick}
134
- {onkeydown}
178
+ onkeydown={handleRightImageKeyDown}
135
179
  data-pw={header.buttonTestId}
136
180
  >
137
181
  <img class="header-right-img" src={header.rightImage} alt="" />
@@ -186,6 +230,8 @@
186
230
 
187
231
  .overlay-active {
188
232
  background-color: var(--background-color, #00000066);
233
+ backdrop-filter: var(--modal-overlay-backdrop-filter, none);
234
+ -webkit-backdrop-filter: var(--modal-overlay-backdrop-filter, none);
189
235
  pointer-events: auto;
190
236
  }
191
237
 
@@ -3,7 +3,8 @@ import type { ModalTransition } from '../types';
3
3
  import type { Snippet } from 'svelte';
4
4
  export type ModalSize = 'large' | 'medium' | 'small' | 'fit-content';
5
5
  export type ModalAlign = 'top' | 'center' | 'bottom';
6
- export type ModalProperties = ModalEventProperties & {
6
+ export type ModalProperties = OptionalModalProperties & ModalEventProperties;
7
+ export type OptionalModalProperties = {
7
8
  size?: ModalSize;
8
9
  align?: ModalAlign;
9
10
  showOverlay?: boolean;
@@ -27,6 +28,10 @@ export type ModalProperties = ModalEventProperties & {
27
28
  content?: Snippet;
28
29
  footerSnippet?: Snippet;
29
30
  classes?: string;
31
+ /** CSS value applied to backdrop-filter on the overlay (e.g. "blur(6px)"). Exposed as --modal-overlay-backdrop-filter CSS var. Default: none (no blur). */
32
+ overlayBackdropFilter?: string;
33
+ /** When true, mounts the overlay at document.body to escape clipping/stacking contexts. Default: false. */
34
+ usePortal?: boolean;
30
35
  };
31
36
  export type ModalEventProperties = {
32
37
  onclose?: () => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.51.0",
3
+ "version": "2.53.0",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",