@juspay/svelte-ui-components 2.51.0 → 2.52.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.
@@ -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.52.0",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",