@juspay/svelte-ui-components 4.7.0 → 4.8.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.
@@ -1,7 +1,9 @@
1
1
  <script lang="ts">
2
+ import { onDestroy } from 'svelte';
2
3
  import type { SnippetProperties } from './properties';
3
4
  import Button from '../Button/Button.svelte';
4
5
  import copySvg from '../assets/copy.svg?raw';
6
+ import { createCopyResetTimer } from './copyResetTimer';
5
7
 
6
8
  let {
7
9
  text,
@@ -9,20 +11,31 @@
9
11
  showCopyButton = true,
10
12
  testId,
11
13
  copyIcon,
14
+ copiedLabel = 'Copied!',
15
+ copyResetMs = 2000,
12
16
  oncopy,
13
17
  classes
14
18
  }: SnippetProperties = $props();
15
19
 
16
20
  let copied = $state(false);
17
21
 
22
+ // The reset timer used to be a bare setTimeout with nothing clearing it: a
23
+ // second copy before the first one's timer fired left two timers racing to
24
+ // flip `copied`, and navigating away mid-flash left the timer armed against
25
+ // a destroyed component. `createCopyResetTimer` (unit-tested on its own in
26
+ // copyResetTimer.test.ts) guarantees only one timer is ever pending, and
27
+ // onDestroy guarantees it never outlives the component it targets.
28
+ const resetTimer = createCopyResetTimer();
29
+ onDestroy(resetTimer.cancel);
30
+
18
31
  async function handleCopy() {
19
32
  try {
20
33
  await navigator.clipboard.writeText(text);
21
34
  copied = true;
22
35
  oncopy?.();
23
- setTimeout(() => {
36
+ resetTimer.arm(() => {
24
37
  copied = false;
25
- }, 2000);
38
+ }, copyResetMs);
26
39
  } catch {
27
40
  // Clipboard API unavailable (non-secure context, iframe restrictions)
28
41
  }
@@ -42,7 +55,7 @@
42
55
  <div class="snippet-copy">
43
56
  <Button onclick={handleCopy} ariaLabel="Copy to clipboard">
44
57
  {#if copied}
45
- <span class="snippet-copied">Copied!</span>
58
+ <span class="snippet-copied" aria-live="polite">{copiedLabel}</span>
46
59
  {:else if typeof copyIcon === 'function'}
47
60
  {@render copyIcon()}
48
61
  {:else}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Manages the single "revert the copied-feedback flag" timer behind Snippet's
3
+ * copy button (#530). Extracted as a plain, Svelte-free module so the timer
4
+ * safety this fixes -- never leaving two timers racing after a fast double
5
+ * copy, never firing against a component that has already unmounted -- is
6
+ * unit-testable on its own, the same way `Tooltip/tooltip-action.ts` keeps
7
+ * its DOM-timing logic outside the component.
8
+ */
9
+ export type CopyResetTimer = {
10
+ /**
11
+ * (Re)arms the timer: clears any timer already pending, then schedules
12
+ * `onReset` to run after `delayMs`.
13
+ */
14
+ arm: (onReset: () => void, delayMs: number) => void;
15
+ /**
16
+ * Cancels a pending timer, if any. Safe to call when nothing is pending --
17
+ * this is what Snippet calls from `onDestroy`.
18
+ */
19
+ cancel: () => void;
20
+ };
21
+ export declare function createCopyResetTimer(): CopyResetTimer;
@@ -0,0 +1,20 @@
1
+ export function createCopyResetTimer() {
2
+ let handle = null;
3
+ return {
4
+ arm(onReset, delayMs) {
5
+ if (handle !== null) {
6
+ clearTimeout(handle);
7
+ }
8
+ handle = setTimeout(() => {
9
+ handle = null;
10
+ onReset();
11
+ }, delayMs);
12
+ },
13
+ cancel() {
14
+ if (handle !== null) {
15
+ clearTimeout(handle);
16
+ handle = null;
17
+ }
18
+ }
19
+ };
20
+ }
@@ -8,6 +8,10 @@ export type OptionalSnippetProperties = {
8
8
  showCopyButton?: boolean;
9
9
  testId?: string;
10
10
  copyIcon?: Snippet;
11
+ /** Text shown in place of the copy icon after a successful copy. Defaults to `'Copied!'`. */
12
+ copiedLabel?: string;
13
+ /** Milliseconds before the copied feedback reverts to the copy icon. Defaults to `2000`. */
14
+ copyResetMs?: number;
11
15
  classes?: string;
12
16
  };
13
17
  export type SnippetEventProperties = {
package/dist-wc/index.js CHANGED
@@ -17671,21 +17671,40 @@ customElements.define("sui-sheet", create_custom_element(Sheet_wc, {
17671
17671
  }, ["default", "footer"], [], { mode: "open" }));
17672
17672
  //#endregion
17673
17673
  //#region src/lib/assets/copy.svg?raw
17674
- var copy_default = "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" xmlns=\"http://www.w3.org/2000/svg\">\n <rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"></rect>\n <path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"></path>\n</svg>\n", root$30 = /* @__PURE__ */ from_html("<span class=\"snippet-copied svelte-ql5iye\">Copied!</span>"), root_1$27 = /* @__PURE__ */ from_html("<span class=\"snippet-copy-icon svelte-ql5iye\"></span>"), root_2$21 = /* @__PURE__ */ from_html("<div class=\"snippet-copy svelte-ql5iye\"><!></div>"), root_3$19 = /* @__PURE__ */ from_html("<div><code class=\"snippet-code svelte-ql5iye\"><span class=\"snippet-prompt svelte-ql5iye\"> </span> <span class=\"snippet-text svelte-ql5iye\"> </span></code> <!></div>"), $$css$32 = {
17674
+ var copy_default = "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" xmlns=\"http://www.w3.org/2000/svg\">\n <rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"></rect>\n <path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"></path>\n</svg>\n";
17675
+ //#endregion
17676
+ //#region src/lib/Snippet/copyResetTimer.ts
17677
+ function createCopyResetTimer() {
17678
+ let e = null;
17679
+ return {
17680
+ arm(t, n) {
17681
+ e !== null && clearTimeout(e), e = setTimeout(() => {
17682
+ e = null, t();
17683
+ }, n);
17684
+ },
17685
+ cancel() {
17686
+ e !== null && (clearTimeout(e), e = null);
17687
+ }
17688
+ };
17689
+ }
17690
+ //#endregion
17691
+ //#region src/lib/Snippet/Snippet.svelte
17692
+ var root$30 = /* @__PURE__ */ from_html("<span class=\"snippet-copied svelte-ql5iye\" aria-live=\"polite\"> </span>"), root_1$27 = /* @__PURE__ */ from_html("<span class=\"snippet-copy-icon svelte-ql5iye\"></span>"), root_2$21 = /* @__PURE__ */ from_html("<div class=\"snippet-copy svelte-ql5iye\"><!></div>"), root_3$19 = /* @__PURE__ */ from_html("<div><code class=\"snippet-code svelte-ql5iye\"><span class=\"snippet-prompt svelte-ql5iye\"> </span> <span class=\"snippet-text svelte-ql5iye\"> </span></code> <!></div>"), $$css$32 = {
17675
17693
  hash: "svelte-ql5iye",
17676
17694
  code: ".snippet.svelte-ql5iye {display:flex;align-items:center;justify-content:space-between;gap:var(--snippet-gap, 8px);background:var(--snippet-background, #1e1e1e);border:var(--snippet-border, 1px solid #333);border-radius:var(--snippet-border-radius, var(--radius, 4px));padding:var(--snippet-padding, 12px 16px);font-family:var(--snippet-font-family, monospace);font-size:var(--snippet-font-size, 14px);color:var(--snippet-color, #e0e0e0);margin:var(--snippet-margin, 0);}.snippet-code.svelte-ql5iye {display:flex;align-items:center;overflow:hidden;flex:1;min-width:0;}.snippet-prompt.svelte-ql5iye {color:var(--snippet-prompt-color, #888);margin-right:var(--snippet-prompt-margin-right, 8px);flex-shrink:0;}.snippet-text.svelte-ql5iye {color:var(--snippet-text-color, #e0e0e0);font-family:var(--snippet-text-font-family, inherit);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}.snippet-copy.svelte-ql5iye {--button-color: var(--snippet-copy-background, transparent);--button-text-color: var(--snippet-copy-color, #888);--button-border: var(--snippet-copy-border, none);--button-padding: var(--snippet-copy-padding, 4px);--button-border-radius: var(--snippet-copy-border-radius, var(--radius, 4px));--cursor: var(--snippet-copy-cursor, pointer);--button-hover-color: var(--snippet-copy-hover-background, #333);display:flex;align-items:center;justify-content:center;flex-shrink:0;}.snippet-copy-icon.svelte-ql5iye {display:inline-flex;align-items:center;justify-content:center;width:var(--snippet-copy-size, 16px);height:var(--snippet-copy-size, 16px);}.snippet-copy-icon.svelte-ql5iye svg {width:100%;height:100%;}.snippet-copied.svelte-ql5iye {color:var(--snippet-copied-color, #4caf50);font-size:var(--snippet-copied-font-size, 12px);font-family:var(--snippet-font-family, monospace);}"
17677
17695
  };
17678
17696
  function Snippet(e, t) {
17679
17697
  push(t, !0), append_styles$1(e, $$css$32);
17680
- let n = prop(t, "text", 7), i = prop(t, "prompt", 7, "$"), a = prop(t, "showCopyButton", 7, !0), o = prop(t, "testId", 7), s = prop(t, "copyIcon", 7), c = prop(t, "oncopy", 7), l = prop(t, "classes", 7), u = /* @__PURE__ */ state(!1);
17681
- async function f() {
17698
+ let n = prop(t, "text", 7), i = prop(t, "prompt", 7, "$"), a = prop(t, "showCopyButton", 7, !0), o = prop(t, "testId", 7), s = prop(t, "copyIcon", 7), c = prop(t, "copiedLabel", 7, "Copied!"), l = prop(t, "copyResetMs", 7, 2e3), u = prop(t, "oncopy", 7), f = prop(t, "classes", 7), p = /* @__PURE__ */ state(!1), h = createCopyResetTimer();
17699
+ onDestroy(h.cancel);
17700
+ async function S() {
17682
17701
  try {
17683
- await navigator.clipboard.writeText(n()), set(u, !0), c()?.(), setTimeout(() => {
17684
- set(u, !1);
17685
- }, 2e3);
17702
+ await navigator.clipboard.writeText(n()), set(p, !0), u()?.(), h.arm(() => {
17703
+ set(p, !1);
17704
+ }, l());
17686
17705
  } catch {}
17687
17706
  }
17688
- var p = {
17707
+ var C = {
17689
17708
  get text() {
17690
17709
  return n();
17691
17710
  },
@@ -17716,49 +17735,62 @@ function Snippet(e, t) {
17716
17735
  set copyIcon(e) {
17717
17736
  s(e), flushSync();
17718
17737
  },
17719
- get oncopy() {
17738
+ get copiedLabel() {
17720
17739
  return c();
17721
17740
  },
17722
- set oncopy(e) {
17741
+ set copiedLabel(e = "Copied!") {
17723
17742
  c(e), flushSync();
17724
17743
  },
17725
- get classes() {
17744
+ get copyResetMs() {
17726
17745
  return l();
17727
17746
  },
17728
- set classes(e) {
17747
+ set copyResetMs(e = 2e3) {
17729
17748
  l(e), flushSync();
17749
+ },
17750
+ get oncopy() {
17751
+ return u();
17752
+ },
17753
+ set oncopy(e) {
17754
+ u(e), flushSync();
17755
+ },
17756
+ get classes() {
17757
+ return f();
17758
+ },
17759
+ set classes(e) {
17760
+ f(e), flushSync();
17730
17761
  }
17731
- }, h = root_3$19(), S = child(h), C = child(S), k = child(C, !0);
17732
- reset(C);
17733
- var R = sibling(C, 2), te = child(R, !0);
17734
- reset(R), reset(S);
17735
- var ne = sibling(S, 2), re = (e) => {
17762
+ }, k = root_3$19(), R = child(k), te = child(R), ne = child(te, !0);
17763
+ reset(te);
17764
+ var re = sibling(te, 2), G = child(re, !0);
17765
+ reset(re), reset(R);
17766
+ var be = sibling(R, 2), Me = (e) => {
17736
17767
  var t = root_2$21();
17737
17768
  Button(child(t), {
17738
- onclick: f,
17769
+ onclick: S,
17739
17770
  ariaLabel: "Copy to clipboard",
17740
17771
  children: (e, t) => {
17741
17772
  var n = comment(), i = first_child(n), a = (e) => {
17742
- append(e, root$30());
17773
+ var t = root$30(), n = child(t, !0);
17774
+ reset(t), template_effect(() => set_text(n, c())), append(e, t);
17743
17775
  }, o = (e) => {
17744
17776
  var t = comment();
17745
17777
  snippet(first_child(t), s), append(e, t);
17746
- }, c = (e) => {
17778
+ }, l = (e) => {
17747
17779
  var t = root_1$27();
17748
17780
  html(t, () => copy_default, !0), reset(t), append(e, t);
17749
17781
  };
17750
17782
  if_block(i, (e) => {
17751
- get(u) ? e(a) : typeof s() == "function" ? e(o, 1) : e(c, -1);
17783
+ get(p) ? e(a) : typeof s() == "function" ? e(o, 1) : e(l, -1);
17752
17784
  }), append(e, n);
17753
17785
  },
17754
17786
  $$slots: { default: !0 }
17755
17787
  }), reset(t), append(e, t);
17756
17788
  };
17757
- return if_block(ne, (e) => {
17758
- a() && e(re);
17759
- }), reset(h), template_effect(() => {
17760
- set_class(h, 1, `snippet ${l() ?? "" ?? ""}`, "svelte-ql5iye"), set_attribute(h, "data-pw", typeof o() == "string" ? o() : null), set_attribute(h, "testid", typeof o() == "string" ? o() : null), set_text(k, i()), set_text(te, n());
17761
- }), append(e, h), pop(p);
17789
+ return if_block(be, (e) => {
17790
+ a() && e(Me);
17791
+ }), reset(k), template_effect(() => {
17792
+ set_class(k, 1, `snippet ${f() ?? "" ?? ""}`, "svelte-ql5iye"), set_attribute(k, "data-pw", typeof o() == "string" ? o() : null), set_attribute(k, "testid", typeof o() == "string" ? o() : null), set_text(ne, i()), set_text(G, n());
17793
+ }), append(e, k), pop(C);
17762
17794
  }
17763
17795
  create_custom_element(Snippet, {
17764
17796
  text: {},
@@ -17766,6 +17798,8 @@ create_custom_element(Snippet, {
17766
17798
  showCopyButton: {},
17767
17799
  testId: {},
17768
17800
  copyIcon: {},
17801
+ copiedLabel: {},
17802
+ copyResetMs: {},
17769
17803
  oncopy: {},
17770
17804
  classes: {}
17771
17805
  }, [], [], { mode: "open" });
@@ -17805,6 +17839,16 @@ customElements.define("sui-snippet", create_custom_element(Snippet_wc, {
17805
17839
  attribute: "test-id",
17806
17840
  type: "String"
17807
17841
  },
17842
+ copiedLabel: {
17843
+ attribute: "copied-label",
17844
+ reflect: !0,
17845
+ type: "String"
17846
+ },
17847
+ copyResetMs: {
17848
+ attribute: "copy-reset-ms",
17849
+ reflect: !0,
17850
+ type: "Number"
17851
+ },
17808
17852
  classes: { type: "String" },
17809
17853
  oncopy: { type: "Object" },
17810
17854
  copyIcon: { type: "Object" }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "4.7.0",
3
+ "version": "4.8.0",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",