@juspay/svelte-ui-components 4.34.1 → 4.35.1

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.
@@ -32,10 +32,12 @@
32
32
  title,
33
33
  description,
34
34
  sections,
35
+ details,
35
36
  functionArguments,
36
37
  hiddenKeys,
37
38
  onconfirm,
38
39
  confirmLabel = 'Confirm',
40
+ confirmDisabled = false,
39
41
  cancelLabel = 'Cancel',
40
42
  showCancel = true,
41
43
  showConfirm = true,
@@ -90,18 +92,19 @@
90
92
  let autoCancelTimeout: ReturnType<typeof setTimeout> | null = null;
91
93
  let originalMicState: boolean | null = null;
92
94
 
95
+ // Expiry from the caller takes precedence, including during a live request.
93
96
  // History mode renders the settled state from props; live mode from local state.
94
97
  // A history card with no initialState renders as expired rather than falling
95
98
  // through to the pending interactive card — a replayed card must never be able
96
99
  // to fire onconfirm.
97
100
  const userResponse = $derived.by((): HITLResponse | null => {
101
+ if (initialState?.status === 'EXPIRED') {
102
+ return 'expired';
103
+ }
98
104
  if (isHistoryMode) {
99
105
  if (initialState === null) {
100
106
  return 'expired';
101
107
  }
102
- if (initialState.status === 'EXPIRED') {
103
- return 'expired';
104
- }
105
108
  return initialState.approved === true ? 'approved' : 'rejected';
106
109
  }
107
110
  return localResponse;
@@ -109,6 +112,7 @@
109
112
  const isCompleted = $derived(userResponse !== null);
110
113
  const isApproved = $derived(userResponse === 'approved' || userResponse === 'auto-approved');
111
114
  const elapsedTime = $derived(countdownSeconds - timeRemaining);
115
+ const approvalBlocked = (action: HITLAction): boolean => action !== 'rejected' && confirmDisabled;
112
116
 
113
117
  const completionText = $derived(
114
118
  userResponse === 'auto-approved'
@@ -136,7 +140,11 @@
136
140
  };
137
141
 
138
142
  const restoreMicState = async (): Promise<void> => {
139
- if (onmictoggle !== null && originalMicState !== null && isMicMuted !== originalMicState) {
143
+ // Consume restoration before awaiting: a failed decision may be retried
144
+ // before the caller has updated isMicMuted, but must not toggle twice.
145
+ const stateToRestore = originalMicState;
146
+ originalMicState = null;
147
+ if (onmictoggle !== null && stateToRestore !== null && isMicMuted !== stateToRestore) {
140
148
  try {
141
149
  await onmictoggle();
142
150
  } catch {
@@ -151,14 +159,16 @@
151
159
  // updated in between.
152
160
  let decisionPending = false;
153
161
 
154
- const settle = (action: HITLAction, detail: HITLSettleDetail = {}): void => {
155
- if (isProcessing || isCompleted) {
162
+ const settle = async (action: HITLAction, detail: HITLSettleDetail = {}): Promise<void> => {
163
+ // The controlled state may have changed while mic restoration was pending.
164
+ if (isCompleted || approvalBlocked(action)) {
165
+ if (approvalBlocked(action)) {
166
+ decisionPending = false;
167
+ }
156
168
  return;
157
169
  }
158
- localResponse = action;
159
- isProcessing = true;
160
170
  try {
161
- onconfirm?.({
171
+ await onconfirm?.({
162
172
  confirmationId,
163
173
  action,
164
174
  approved: action !== 'rejected',
@@ -168,30 +178,46 @@
168
178
  ...(typeof detail.message === 'string' ? { message: detail.message } : {}),
169
179
  ...(Array.isArray(detail.answers) ? { answers: detail.answers } : {})
170
180
  });
181
+ // Only publish success after the handoff succeeds. History/expiry updates
182
+ // received during the request remain authoritative.
183
+ if (!isCompleted && !approvalBlocked(action)) {
184
+ localResponse = action;
185
+ } else if (approvalBlocked(action)) {
186
+ decisionPending = false;
187
+ }
171
188
  } catch {
172
189
  // The decision failed to hand off; release the card for another attempt.
173
- localResponse = null;
174
190
  decisionPending = false;
175
- } finally {
176
- isProcessing = false;
177
191
  }
178
192
  };
179
193
 
180
194
  const decide = async (action: HITLAction, detail: HITLSettleDetail = {}): Promise<void> => {
181
- if (decisionPending || isCompleted) {
195
+ if (decisionPending || isCompleted || approvalBlocked(action)) {
182
196
  return;
183
197
  }
184
198
  decisionPending = true;
185
- await restoreMicState();
186
- settle(action, detail);
199
+ isProcessing = true;
200
+ try {
201
+ await restoreMicState();
202
+ if (approvalBlocked(action)) {
203
+ decisionPending = false;
204
+ return;
205
+ }
206
+ await settle(action, detail);
207
+ } finally {
208
+ isProcessing = false;
209
+ }
187
210
  };
188
211
 
189
212
  const startCountdown = (): void => {
213
+ if (confirmDisabled) {
214
+ return;
215
+ }
190
216
  countdownActive = true;
191
217
  timeRemaining = countdownSeconds;
192
218
  countdownInterval = setInterval(() => {
193
219
  // A sibling card's interaction pauses this countdown on its next tick.
194
- if (get(pauseAllConfirmationTimers)) {
220
+ if (confirmDisabled || get(pauseAllConfirmationTimers)) {
195
221
  stopCountdown();
196
222
  return;
197
223
  }
@@ -211,6 +237,9 @@
211
237
  };
212
238
 
213
239
  const interact = async (action: HITLAction, detail: HITLSettleDetail = {}): Promise<void> => {
240
+ if (approvalBlocked(action)) {
241
+ return;
242
+ }
214
243
  pauseTimers();
215
244
  await decide(action, detail);
216
245
  };
@@ -323,7 +352,9 @@
323
352
  }
324
353
  pauseAllConfirmationTimers.set(false);
325
354
  if (countdownSeconds > 0) {
326
- startCountdown();
355
+ if (!confirmDisabled) {
356
+ startCountdown();
357
+ }
327
358
  } else if (autoCancelSeconds > 0) {
328
359
  autoCancelTimeout = setTimeout(() => {
329
360
  void decide('rejected');
@@ -472,12 +503,18 @@
472
503
  {/if}
473
504
 
474
505
  <div class="confirmation-body">
475
- {#each parameterSections as section, sectionIndex (sectionIndex)}
476
- <div class="params">
477
- <span class="parameter-label">{section.label}</span>
478
- <span class="parameter-value">{section.value}</span>
506
+ {#if typeof details === 'function'}
507
+ <div class="rich-details">
508
+ {@render details()}
479
509
  </div>
480
- {/each}
510
+ {:else}
511
+ {#each parameterSections as section, sectionIndex (sectionIndex)}
512
+ <div class="params">
513
+ <span class="parameter-label">{section.label}</span>
514
+ <span class="parameter-value">{section.value}</span>
515
+ </div>
516
+ {/each}
517
+ {/if}
481
518
 
482
519
  {#if isCompleted && userResponse !== null}
483
520
  <div
@@ -640,14 +677,14 @@
640
677
  {/if}
641
678
  {#if showConfirm}
642
679
  <div class="confirm-button">
643
- {#if countdownActive}
680
+ {#if countdownActive && !confirmDisabled}
644
681
  <div class="progress-anchor">
645
682
  <Progress value={elapsedTime} max={countdownSeconds} />
646
683
  </div>
647
684
  {/if}
648
685
  <Button
649
686
  text={confirmLabel}
650
- enable={!isProcessing}
687
+ enable={!isProcessing && !confirmDisabled}
651
688
  testId={confirmTestId ?? (testId && `${testId}-confirm`)}
652
689
  onclick={() => interact('approved')}
653
690
  />
@@ -718,6 +755,11 @@
718
755
  gap: var(--hitl-param-gap, 0.25rem);
719
756
  }
720
757
 
758
+ .rich-details {
759
+ min-width: 0;
760
+ width: 100%;
761
+ }
762
+
721
763
  .parameter-label {
722
764
  font-size: var(--hitl-param-label-font-size, 0.6875rem);
723
765
  font-weight: var(--hitl-param-label-font-weight, 600);
@@ -106,12 +106,20 @@ export type OptionalHITLProperties = {
106
106
  * formatted generically instead.
107
107
  */
108
108
  sections?: HITLSection[];
109
+ /**
110
+ * Rich parameter detail replacing sections/arguments in pending and resolved
111
+ * cards, including history. Unlike children, this remains after a decision.
112
+ */
113
+ details?: Snippet;
109
114
  /** Raw arguments, formatted generically when no `sections` are given. */
110
115
  functionArguments?: Record<string, unknown>;
111
116
  /** Argument keys (case-insensitive) hidden from the generic formatting. */
112
117
  hiddenKeys?: string[];
113
- onconfirm?: (event: HITLEvent) => void;
118
+ /** Awaited before completion; throwing/rejecting leaves the card pending for retry. */
119
+ onconfirm?: (event: HITLEvent) => void | Promise<void>;
114
120
  confirmLabel?: string;
121
+ /** Blocks manual and timed approval while leaving rejection available. Defaults to false. */
122
+ confirmDisabled?: boolean;
115
123
  cancelLabel?: string;
116
124
  /** Hides the cancel button entirely — for a card whose only real dispositions come through `actions`. Default `true`. */
117
125
  showCancel?: boolean;
@@ -17,6 +17,7 @@
17
17
  searchable = false,
18
18
  searchPosition = 'trigger',
19
19
  placeholder = '',
20
+ ariaLabel,
20
21
  disabled = false,
21
22
  error = false,
22
23
  errorMessage,
@@ -754,6 +755,7 @@
754
755
  onclick={handleTriggerClick}
755
756
  onkeydown={handleKeydown}
756
757
  role="combobox"
758
+ aria-label={ariaLabel}
757
759
  aria-expanded={open}
758
760
  aria-haspopup="listbox"
759
761
  aria-controls={listboxId}
@@ -779,6 +781,7 @@
779
781
  <input
780
782
  class="select-search"
781
783
  type="text"
784
+ aria-label={ariaLabel}
782
785
  value={query}
783
786
  oninput={handleSearchInput}
784
787
  onfocus={handleSearchFocus}
@@ -807,6 +810,7 @@
807
810
  <input
808
811
  class="select-search"
809
812
  type="text"
813
+ aria-label={ariaLabel}
810
814
  value={query}
811
815
  oninput={handleSearchInput}
812
816
  onfocus={handleSearchFocus}
@@ -828,6 +832,7 @@
828
832
  <input
829
833
  class="select-search"
830
834
  type="text"
835
+ aria-label={ariaLabel}
831
836
  value={open ? query : displayText}
832
837
  oninput={handleSearchInput}
833
838
  onfocus={handleSearchFocus}
@@ -55,6 +55,8 @@ export type OptionalSelectProperties = {
55
55
  */
56
56
  searchPosition?: SelectSearchPosition;
57
57
  placeholder?: string;
58
+ /** Accessible name for the combobox trigger. Omitted to preserve the default unnamed behavior. */
59
+ ariaLabel?: string;
58
60
  disabled?: boolean;
59
61
  /**
60
62
  * Renders the error treatment on the trigger — a red border that outranks
@@ -33,7 +33,7 @@ function startedInside(event, node) {
33
33
  return eventHitsInside(node, event);
34
34
  }
35
35
  function handleKeydown(event) {
36
- if (event.key !== 'Escape') {
36
+ if (event.key !== 'Escape' || event.isComposing || event.keyCode === 229) {
37
37
  return;
38
38
  }
39
39
  topmost()?.onEscape?.();