@thoughtspot/visual-embed-sdk 1.49.1 → 1.49.3

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.
Files changed (151) hide show
  1. package/cjs/package.json +4 -10
  2. package/cjs/src/css-variables.d.ts +17 -0
  3. package/cjs/src/css-variables.d.ts.map +1 -1
  4. package/cjs/src/embed/app.d.ts +7 -1
  5. package/cjs/src/embed/app.d.ts.map +1 -1
  6. package/cjs/src/embed/app.js +4 -1
  7. package/cjs/src/embed/app.js.map +1 -1
  8. package/cjs/src/embed/app.spec.js +97 -0
  9. package/cjs/src/embed/app.spec.js.map +1 -1
  10. package/cjs/src/embed/conversation.d.ts +8 -0
  11. package/cjs/src/embed/conversation.d.ts.map +1 -1
  12. package/cjs/src/embed/conversation.js +2 -1
  13. package/cjs/src/embed/conversation.js.map +1 -1
  14. package/cjs/src/embed/conversation.spec.js +12 -0
  15. package/cjs/src/embed/conversation.spec.js.map +1 -1
  16. package/cjs/src/embed/events.spec.js +5 -0
  17. package/cjs/src/embed/events.spec.js.map +1 -1
  18. package/cjs/src/embed/liveboard.d.ts +6 -0
  19. package/cjs/src/embed/liveboard.d.ts.map +1 -1
  20. package/cjs/src/embed/liveboard.js +4 -1
  21. package/cjs/src/embed/liveboard.js.map +1 -1
  22. package/cjs/src/embed/liveboard.spec.js +11 -0
  23. package/cjs/src/embed/liveboard.spec.js.map +1 -1
  24. package/cjs/src/embed/spotter-viz-utils.d.ts +29 -0
  25. package/cjs/src/embed/spotter-viz-utils.d.ts.map +1 -1
  26. package/cjs/src/embed/spotter-viz-utils.js.map +1 -1
  27. package/cjs/src/embed/ts-embed.d.ts +38 -0
  28. package/cjs/src/embed/ts-embed.d.ts.map +1 -1
  29. package/cjs/src/embed/ts-embed.js +166 -9
  30. package/cjs/src/embed/ts-embed.js.map +1 -1
  31. package/cjs/src/embed/ts-embed.spec.js +237 -0
  32. package/cjs/src/embed/ts-embed.spec.js.map +1 -1
  33. package/cjs/src/index.d.ts +2 -2
  34. package/cjs/src/index.d.ts.map +1 -1
  35. package/cjs/src/index.js.map +1 -1
  36. package/cjs/src/types.d.ts +87 -4
  37. package/cjs/src/types.d.ts.map +1 -1
  38. package/cjs/src/types.js +47 -3
  39. package/cjs/src/types.js.map +1 -1
  40. package/cjs/src/utils/graphql/answerService/answerService.spec.js +15 -0
  41. package/cjs/src/utils/graphql/answerService/answerService.spec.js.map +1 -1
  42. package/cjs/src/utils/processTrigger.d.ts.map +1 -1
  43. package/cjs/src/utils/processTrigger.js +6 -5
  44. package/cjs/src/utils/processTrigger.js.map +1 -1
  45. package/cjs/src/utils/processTrigger.spec.js +13 -0
  46. package/cjs/src/utils/processTrigger.spec.js.map +1 -1
  47. package/cjs/src/utils.d.ts +10 -0
  48. package/cjs/src/utils.d.ts.map +1 -1
  49. package/cjs/src/utils.js +25 -4
  50. package/cjs/src/utils.js.map +1 -1
  51. package/cjs/src/utils.spec.js +48 -0
  52. package/cjs/src/utils.spec.js.map +1 -1
  53. package/dist/{index-B6Rn561t.js → index-DsSQndIB.js} +303 -187
  54. package/dist/src/css-variables.d.ts +17 -0
  55. package/dist/src/css-variables.d.ts.map +1 -1
  56. package/dist/src/embed/app.d.ts +7 -1
  57. package/dist/src/embed/app.d.ts.map +1 -1
  58. package/dist/src/embed/conversation.d.ts +8 -0
  59. package/dist/src/embed/conversation.d.ts.map +1 -1
  60. package/dist/src/embed/liveboard.d.ts +6 -0
  61. package/dist/src/embed/liveboard.d.ts.map +1 -1
  62. package/dist/src/embed/spotter-viz-utils.d.ts +29 -0
  63. package/dist/src/embed/spotter-viz-utils.d.ts.map +1 -1
  64. package/dist/src/embed/ts-embed.d.ts +38 -0
  65. package/dist/src/embed/ts-embed.d.ts.map +1 -1
  66. package/dist/src/index.d.ts +2 -2
  67. package/dist/src/index.d.ts.map +1 -1
  68. package/dist/src/types.d.ts +87 -4
  69. package/dist/src/types.d.ts.map +1 -1
  70. package/dist/src/utils/processTrigger.d.ts.map +1 -1
  71. package/dist/src/utils.d.ts +10 -0
  72. package/dist/src/utils.d.ts.map +1 -1
  73. package/dist/tsembed-react.es.js +282 -35
  74. package/dist/tsembed-react.js +583 -220
  75. package/dist/tsembed.es.js +282 -35
  76. package/dist/tsembed.js +583 -220
  77. package/dist/visual-embed-sdk-react-full.d.ts +190 -4
  78. package/dist/visual-embed-sdk-react.d.ts +190 -4
  79. package/dist/visual-embed-sdk.d.ts +190 -4
  80. package/lib/package.json +4 -10
  81. package/lib/src/css-variables.d.ts +17 -0
  82. package/lib/src/css-variables.d.ts.map +1 -1
  83. package/lib/src/embed/app.d.ts +7 -1
  84. package/lib/src/embed/app.d.ts.map +1 -1
  85. package/lib/src/embed/app.js +4 -1
  86. package/lib/src/embed/app.js.map +1 -1
  87. package/lib/src/embed/app.spec.js +97 -0
  88. package/lib/src/embed/app.spec.js.map +1 -1
  89. package/lib/src/embed/conversation.d.ts +8 -0
  90. package/lib/src/embed/conversation.d.ts.map +1 -1
  91. package/lib/src/embed/conversation.js +2 -1
  92. package/lib/src/embed/conversation.js.map +1 -1
  93. package/lib/src/embed/conversation.spec.js +12 -0
  94. package/lib/src/embed/conversation.spec.js.map +1 -1
  95. package/lib/src/embed/events.spec.js +5 -0
  96. package/lib/src/embed/events.spec.js.map +1 -1
  97. package/lib/src/embed/liveboard.d.ts +6 -0
  98. package/lib/src/embed/liveboard.d.ts.map +1 -1
  99. package/lib/src/embed/liveboard.js +4 -1
  100. package/lib/src/embed/liveboard.js.map +1 -1
  101. package/lib/src/embed/liveboard.spec.js +11 -0
  102. package/lib/src/embed/liveboard.spec.js.map +1 -1
  103. package/lib/src/embed/spotter-viz-utils.d.ts +29 -0
  104. package/lib/src/embed/spotter-viz-utils.d.ts.map +1 -1
  105. package/lib/src/embed/spotter-viz-utils.js.map +1 -1
  106. package/lib/src/embed/ts-embed.d.ts +38 -0
  107. package/lib/src/embed/ts-embed.d.ts.map +1 -1
  108. package/lib/src/embed/ts-embed.js +167 -10
  109. package/lib/src/embed/ts-embed.js.map +1 -1
  110. package/lib/src/embed/ts-embed.spec.js +237 -0
  111. package/lib/src/embed/ts-embed.spec.js.map +1 -1
  112. package/lib/src/index.d.ts +2 -2
  113. package/lib/src/index.d.ts.map +1 -1
  114. package/lib/src/index.js.map +1 -1
  115. package/lib/src/types.d.ts +87 -4
  116. package/lib/src/types.d.ts.map +1 -1
  117. package/lib/src/types.js +47 -3
  118. package/lib/src/types.js.map +1 -1
  119. package/lib/src/utils/graphql/answerService/answerService.spec.js +15 -0
  120. package/lib/src/utils/graphql/answerService/answerService.spec.js.map +1 -1
  121. package/lib/src/utils/processTrigger.d.ts.map +1 -1
  122. package/lib/src/utils/processTrigger.js +6 -5
  123. package/lib/src/utils/processTrigger.js.map +1 -1
  124. package/lib/src/utils/processTrigger.spec.js +13 -0
  125. package/lib/src/utils/processTrigger.spec.js.map +1 -1
  126. package/lib/src/utils.d.ts +10 -0
  127. package/lib/src/utils.d.ts.map +1 -1
  128. package/lib/src/utils.js +22 -1
  129. package/lib/src/utils.js.map +1 -1
  130. package/lib/src/utils.spec.js +49 -1
  131. package/lib/src/utils.spec.js.map +1 -1
  132. package/lib/src/visual-embed-sdk.d.ts +190 -4
  133. package/package.json +4 -10
  134. package/src/css-variables.ts +20 -0
  135. package/src/embed/app.spec.ts +109 -0
  136. package/src/embed/app.ts +11 -1
  137. package/src/embed/conversation.spec.ts +17 -0
  138. package/src/embed/conversation.ts +10 -0
  139. package/src/embed/events.spec.ts +7 -0
  140. package/src/embed/liveboard.spec.ts +15 -0
  141. package/src/embed/liveboard.ts +10 -0
  142. package/src/embed/spotter-viz-utils.ts +30 -0
  143. package/src/embed/ts-embed.spec.ts +294 -0
  144. package/src/embed/ts-embed.ts +180 -9
  145. package/src/index.ts +2 -1
  146. package/src/types.ts +86 -3
  147. package/src/utils/graphql/answerService/answerService.spec.ts +18 -0
  148. package/src/utils/processTrigger.spec.ts +23 -0
  149. package/src/utils/processTrigger.ts +8 -6
  150. package/src/utils.spec.ts +63 -0
  151. package/src/utils.ts +25 -1
@@ -32,6 +32,12 @@ export interface SpotterVizStarterPrompt {
32
32
  * customStarterPrompts: [
33
33
  * { id: '1', displayText: 'Top products', fullPrompt: 'What are the top products by revenue?' }
34
34
  * ],
35
+ * // loaderHeadline and loaderTips require SDK: 1.51.0 | ThoughtSpot Cloud: 26.8.0.cl
36
+ * loaderHeadline: 'Crunching the numbers...',
37
+ * loaderTips: [
38
+ * { label: 'Tip', text: 'try asking about revenue by region' },
39
+ * { label: 'Tip', text: 'use natural language' },
40
+ * ],
35
41
  * },
36
42
  * })
37
43
  * ```
@@ -76,6 +82,30 @@ export interface SpotterVizConfig {
76
82
  * @version SDK: 1.50.0 | ThoughtSpot Cloud: 26.7.0.cl
77
83
  */
78
84
  inputChatPlaceholder?: string;
85
+ /**
86
+ * Custom headline text shown in the SpotterViz loading state.
87
+ * Replaces the default loading headline.
88
+ * @version SDK: 1.51.0 | ThoughtSpot Cloud: 26.8.0.cl
89
+ */
90
+ loaderHeadline?: string;
91
+ /**
92
+ * Custom tips shown in the SpotterViz loading state.
93
+ * Replaces the default loading tips with the provided list.
94
+ * @version SDK: 1.51.0 | ThoughtSpot Cloud: 26.8.0.cl
95
+ */
96
+ loaderTips?: SpotterVizLoaderTip[];
97
+ }
98
+
99
+ /**
100
+ * A single tip shown in the SpotterViz loading state.
101
+ * @version SDK: 1.51.0 | ThoughtSpot Cloud: 26.8.0.cl
102
+ * @group Embed components
103
+ */
104
+ export interface SpotterVizLoaderTip {
105
+ /** Short label rendered alongside the tip (e.g. "Tip"). */
106
+ label: string;
107
+ /** Tip body text. */
108
+ text: string;
79
109
  }
80
110
 
81
111
  export function buildSpotterVizAppInitData<T extends DefaultAppInitData>(
@@ -2479,6 +2479,10 @@ describe('Unit test case for ts embed', () => {
2479
2479
  // The ResizeObserver now tracks the placeholder inside this.el,
2480
2480
  // not this.el itself, so pass it as the target.
2481
2481
  const preRenderPlaceholder = tsEmbedDiv.firstElementChild as HTMLElement;
2482
+ preRenderPlaceholder.getBoundingClientRect = jest.fn().mockReturnValue({
2483
+ x: 0, y: 0, width: 987, height: 297,
2484
+ top: 0, left: 0, bottom: 297, right: 987,
2485
+ });
2482
2486
  resizeObserverCb([
2483
2487
  {
2484
2488
  target: preRenderPlaceholder,
@@ -2660,6 +2664,296 @@ describe('Unit test case for ts embed', () => {
2660
2664
  );
2661
2665
  (logger.error as any).mockClear();
2662
2666
  });
2667
+
2668
+ describe('preRenderContainer', () => {
2669
+ it('should append preRenderWrapper to document.body when preRenderContainer is not set', async () => {
2670
+ createRootEleForEmbed();
2671
+
2672
+ const libEmbed = new LiveboardEmbed('#tsEmbedDiv', {
2673
+ preRenderId: 'container-default',
2674
+ liveboardId: 'myLiveboardId',
2675
+ });
2676
+ await libEmbed.preRender();
2677
+
2678
+ const preRenderIds = libEmbed.getPreRenderIds();
2679
+ const preRenderWrapper = document.getElementById(preRenderIds.wrapper);
2680
+ expect(document.body.contains(preRenderWrapper)).toBe(true);
2681
+ });
2682
+
2683
+ it('should append preRenderWrapper to the element matching a CSS selector string', async () => {
2684
+ createRootEleForEmbed();
2685
+ const customContainer = document.createElement('div');
2686
+ customContainer.id = 'custom-pre-render-container';
2687
+ document.body.appendChild(customContainer);
2688
+
2689
+ const libEmbed = new LiveboardEmbed('#tsEmbedDiv', {
2690
+ preRenderId: 'container-selector',
2691
+ liveboardId: 'myLiveboardId',
2692
+ preRenderContainer: '#custom-pre-render-container',
2693
+ });
2694
+ await libEmbed.preRender();
2695
+
2696
+ const preRenderIds = libEmbed.getPreRenderIds();
2697
+ const preRenderWrapper = document.getElementById(preRenderIds.wrapper);
2698
+ expect(customContainer.contains(preRenderWrapper)).toBe(true);
2699
+
2700
+ customContainer.remove();
2701
+ });
2702
+
2703
+ it('should fall back to document.body when the CSS selector string matches nothing', async () => {
2704
+ createRootEleForEmbed();
2705
+
2706
+ const libEmbed = new LiveboardEmbed('#tsEmbedDiv', {
2707
+ preRenderId: 'container-selector-miss',
2708
+ liveboardId: 'myLiveboardId',
2709
+ preRenderContainer: '#does-not-exist',
2710
+ });
2711
+ await libEmbed.preRender();
2712
+
2713
+ const preRenderIds = libEmbed.getPreRenderIds();
2714
+ const preRenderWrapper = document.getElementById(preRenderIds.wrapper);
2715
+ expect(document.body.contains(preRenderWrapper)).toBe(true);
2716
+ });
2717
+
2718
+ it('should fall back to document.body and log an error when the CSS selector is invalid', async () => {
2719
+ createRootEleForEmbed();
2720
+ jest.spyOn(logger, 'error').mockImplementation(jest.fn());
2721
+
2722
+ const libEmbed = new LiveboardEmbed('#tsEmbedDiv', {
2723
+ preRenderId: 'container-selector-invalid',
2724
+ liveboardId: 'myLiveboardId',
2725
+ preRenderContainer: '###invalid###',
2726
+ });
2727
+ await libEmbed.preRender();
2728
+
2729
+ const preRenderIds = libEmbed.getPreRenderIds();
2730
+ const preRenderWrapper = document.getElementById(preRenderIds.wrapper);
2731
+ expect(document.body.contains(preRenderWrapper)).toBe(true);
2732
+ expect(logger.error).toHaveBeenCalledWith(
2733
+ expect.stringContaining('Invalid CSS selector for preRenderContainer'),
2734
+ expect.any(Error),
2735
+ );
2736
+ (logger.error as any).mockClear();
2737
+ });
2738
+
2739
+ it('should append preRenderWrapper to an Element passed directly as preRenderContainer', async () => {
2740
+ createRootEleForEmbed();
2741
+ const customContainer = document.createElement('div');
2742
+ customContainer.id = 'custom-pre-render-element';
2743
+ document.body.appendChild(customContainer);
2744
+
2745
+ const libEmbed = new LiveboardEmbed('#tsEmbedDiv', {
2746
+ preRenderId: 'container-element',
2747
+ liveboardId: 'myLiveboardId',
2748
+ preRenderContainer: customContainer,
2749
+ });
2750
+ await libEmbed.preRender();
2751
+
2752
+ const preRenderIds = libEmbed.getPreRenderIds();
2753
+ const preRenderWrapper = document.getElementById(preRenderIds.wrapper);
2754
+ expect(customContainer.contains(preRenderWrapper)).toBe(true);
2755
+
2756
+ customContainer.remove();
2757
+ });
2758
+
2759
+ it('should set the container position to relative when it is static', async () => {
2760
+ createRootEleForEmbed();
2761
+ const customContainer = document.createElement('div');
2762
+ customContainer.id = 'custom-pre-render-static';
2763
+ customContainer.style.position = 'static';
2764
+ document.body.appendChild(customContainer);
2765
+
2766
+ const libEmbed = new LiveboardEmbed('#tsEmbedDiv', {
2767
+ preRenderId: 'container-position-static',
2768
+ liveboardId: 'myLiveboardId',
2769
+ preRenderContainer: customContainer,
2770
+ });
2771
+ await libEmbed.preRender();
2772
+
2773
+ expect(customContainer.style.position).toBe('relative');
2774
+
2775
+ customContainer.remove();
2776
+ });
2777
+
2778
+ it('should restore the original container position on destroy', async () => {
2779
+ createRootEleForEmbed();
2780
+ const customContainer = document.createElement('div');
2781
+ customContainer.id = 'custom-pre-render-restore';
2782
+ customContainer.style.position = 'static';
2783
+ document.body.appendChild(customContainer);
2784
+
2785
+ const libEmbed = new LiveboardEmbed('#tsEmbedDiv', {
2786
+ preRenderId: 'container-position-restore',
2787
+ liveboardId: 'myLiveboardId',
2788
+ preRenderContainer: customContainer,
2789
+ });
2790
+ await libEmbed.preRender();
2791
+ expect(customContainer.style.position).toBe('relative');
2792
+
2793
+ libEmbed.destroy();
2794
+
2795
+ // Reverts to the exact value the container had before.
2796
+ expect(customContainer.style.position).toBe('static');
2797
+ // The container reference is dropped so a destroyed embed does
2798
+ // not pin the (possibly detached) element in memory.
2799
+ expect((libEmbed as any).preRenderContainerEl).toBeNull();
2800
+
2801
+ customContainer.remove();
2802
+ });
2803
+
2804
+ it('should not touch the container position when it is already positioned', async () => {
2805
+ createRootEleForEmbed();
2806
+ const customContainer = document.createElement('div');
2807
+ customContainer.id = 'custom-pre-render-positioned';
2808
+ customContainer.style.position = 'absolute';
2809
+ document.body.appendChild(customContainer);
2810
+
2811
+ const libEmbed = new LiveboardEmbed('#tsEmbedDiv', {
2812
+ preRenderId: 'container-position-positioned',
2813
+ liveboardId: 'myLiveboardId',
2814
+ preRenderContainer: customContainer,
2815
+ });
2816
+ await libEmbed.preRender();
2817
+ expect(customContainer.style.position).toBe('absolute');
2818
+
2819
+ libEmbed.destroy();
2820
+ expect(customContainer.style.position).toBe('absolute');
2821
+
2822
+ customContainer.remove();
2823
+ });
2824
+
2825
+ it('should keep the container positioned on destroy while another preRender wrapper remains', async () => {
2826
+ createRootEleForEmbed();
2827
+ const customContainer = document.createElement('div');
2828
+ customContainer.id = 'custom-pre-render-shared';
2829
+ customContainer.style.position = 'static';
2830
+ document.body.appendChild(customContainer);
2831
+
2832
+ const firstEmbed = new LiveboardEmbed('#tsEmbedDiv', {
2833
+ preRenderId: 'container-shared-first',
2834
+ liveboardId: 'myLiveboardId',
2835
+ preRenderContainer: customContainer,
2836
+ });
2837
+ await firstEmbed.preRender();
2838
+
2839
+ const secondEmbed = new LiveboardEmbed('#tsEmbedDiv', {
2840
+ preRenderId: 'container-shared-second',
2841
+ liveboardId: 'myLiveboardId',
2842
+ preRenderContainer: customContainer,
2843
+ });
2844
+ await secondEmbed.preRender();
2845
+
2846
+ expect(customContainer.style.position).toBe('relative');
2847
+
2848
+ // Destroying the first embed must not strip the positioning
2849
+ // context the second embed still relies on.
2850
+ firstEmbed.destroy();
2851
+ expect(customContainer.style.position).toBe('relative');
2852
+
2853
+ // Once the last embed is destroyed, the override is reverted.
2854
+ secondEmbed.destroy();
2855
+ expect(customContainer.style.position).toBe('static');
2856
+
2857
+ customContainer.remove();
2858
+ });
2859
+
2860
+ it('should resolve a selector container inside the host shadow root', async () => {
2861
+ const host = document.createElement('div');
2862
+ document.body.appendChild(host);
2863
+ const shadow = host.attachShadow({ mode: 'open' });
2864
+ const hostEl = document.createElement('div');
2865
+ const shadowContainer = document.createElement('div');
2866
+ shadowContainer.id = 'shadow-pre-render-container';
2867
+ shadow.appendChild(hostEl);
2868
+ shadow.appendChild(shadowContainer);
2869
+
2870
+ const libEmbed = new LiveboardEmbed(hostEl, {
2871
+ preRenderId: 'container-shadow',
2872
+ liveboardId: 'myLiveboardId',
2873
+ preRenderContainer: '#shadow-pre-render-container',
2874
+ });
2875
+ await libEmbed.preRender();
2876
+
2877
+ const preRenderIds = libEmbed.getPreRenderIds();
2878
+ const preRenderWrapper = shadow.querySelector(`#${preRenderIds.wrapper}`);
2879
+ // The wrapper lands in the shadow container, not the body.
2880
+ expect(shadowContainer.contains(preRenderWrapper)).toBe(true);
2881
+ expect(document.getElementById(preRenderIds.wrapper)).toBeNull();
2882
+
2883
+ host.remove();
2884
+ });
2885
+
2886
+ it('should re-attach the wrapper when a selector container is remounted', async () => {
2887
+ createRootEleForEmbed();
2888
+ const oldContainer = document.createElement('div');
2889
+ oldContainer.id = 'remount-container';
2890
+ document.body.appendChild(oldContainer);
2891
+
2892
+ const libEmbed = new LiveboardEmbed('#tsEmbedDiv', {
2893
+ preRenderId: 'container-remount',
2894
+ liveboardId: 'myLiveboardId',
2895
+ preRenderContainer: '#remount-container',
2896
+ doNotTrackPreRenderSize: true,
2897
+ });
2898
+ libEmbed.preRender();
2899
+ await waitFor(() => !!getIFrameEl());
2900
+ await libEmbed.showPreRender();
2901
+
2902
+ const preRenderIds = libEmbed.getPreRenderIds();
2903
+ const wrapper = document.getElementById(preRenderIds.wrapper);
2904
+ expect(oldContainer.contains(wrapper)).toBe(true);
2905
+
2906
+ // Simulate React remounting the container: the old node (with
2907
+ // our wrapper) is detached and a fresh node takes its place.
2908
+ const removeSpy = jest.spyOn(oldContainer, 'removeEventListener');
2909
+ oldContainer.remove();
2910
+ const newContainer = document.createElement('div');
2911
+ newContainer.id = 'remount-container';
2912
+ document.body.appendChild(newContainer);
2913
+ const addSpy = jest.spyOn(newContainer, 'addEventListener');
2914
+
2915
+ // A reposition (scroll/resize/re-show) heals the stale ref.
2916
+ libEmbed.syncPreRenderStyle();
2917
+
2918
+ expect(newContainer.contains(wrapper)).toBe(true);
2919
+ expect(oldContainer.contains(wrapper)).toBe(false);
2920
+ // The scroll listener is migrated to the live container.
2921
+ expect(removeSpy).toHaveBeenCalledWith('scroll', expect.any(Function));
2922
+ expect(addSpy).toHaveBeenCalledWith('scroll', expect.any(Function));
2923
+
2924
+ libEmbed.destroy();
2925
+ newContainer.remove();
2926
+ });
2927
+
2928
+ it('should leave an element-passed container untouched when it is detached', async () => {
2929
+ createRootEleForEmbed();
2930
+ const container = document.createElement('div');
2931
+ document.body.appendChild(container);
2932
+
2933
+ const libEmbed = new LiveboardEmbed('#tsEmbedDiv', {
2934
+ preRenderId: 'container-element-remount',
2935
+ liveboardId: 'myLiveboardId',
2936
+ preRenderContainer: container,
2937
+ doNotTrackPreRenderSize: true,
2938
+ });
2939
+ libEmbed.preRender();
2940
+ await waitFor(() => !!getIFrameEl());
2941
+ await libEmbed.showPreRender();
2942
+
2943
+ const preRenderIds = libEmbed.getPreRenderIds();
2944
+ const wrapper = document.getElementById(preRenderIds.wrapper);
2945
+ expect(container.contains(wrapper)).toBe(true);
2946
+
2947
+ // Detach the element container; it cannot be re-resolved, so
2948
+ // the wrapper stays with it (no crash, no body fallback).
2949
+ container.remove();
2950
+ libEmbed.syncPreRenderStyle();
2951
+
2952
+ expect(container.contains(wrapper)).toBe(true);
2953
+
2954
+ libEmbed.destroy();
2955
+ });
2956
+ });
2663
2957
  });
2664
2958
 
2665
2959
  describe('IdleSessionTimeout embedEvent for TrustedAuthTokenCookieless authType with autoLogin true', () => {
@@ -28,6 +28,7 @@ import {
28
28
  getCustomisations,
29
29
  getRuntimeFilters,
30
30
  getDOMNode,
31
+ querySelectorAcrossShadowRoot,
31
32
  getFilterQuery,
32
33
  getQueryParamString,
33
34
  getRuntimeParameters,
@@ -95,6 +96,14 @@ import {
95
96
  */
96
97
  export const THOUGHTSPOT_PARAM_PREFIX = 'ts-';
97
98
  const TS_EMBED_ID = '_thoughtspot-embed';
99
+ /**
100
+ * dataset key used to stash a custom preRenderContainer's original inline
101
+ * `position` while we override it to `relative`. Stored on the container (not
102
+ * per-instance) so the override can be reverted on destroy even when multiple
103
+ * pre-rendered embeds share the same container.
104
+ */
105
+ const PRERENDER_CONTAINER_ORIGINAL_POSITION_KEY = 'tsEmbedOriginalPosition';
106
+ const PRERENDER_WRAPPER_ID_PREFIX = 'tsEmbed-pre-render-wrapper-';
98
107
 
99
108
  /**
100
109
  * The event id map from v2 event names to v1 event id
@@ -197,6 +206,10 @@ export class TsEmbed {
197
206
 
198
207
  private resizeObserver: ResizeObserver;
199
208
 
209
+ private preRenderContainerEl: HTMLElement | null = null;
210
+
211
+ private containerScrollListener: (() => void) | null = null;
212
+
200
213
  protected hostEventClient: HostEventClient;
201
214
 
202
215
  protected isReadyForRenderPromise;
@@ -1152,6 +1165,91 @@ export class TsEmbed {
1152
1165
  return placeholder;
1153
1166
  }
1154
1167
 
1168
+ /**
1169
+ * Resolves the configured preRenderContainer to a live element, falling
1170
+ * back to `document.body`. A string selector is re-queried on every call so
1171
+ * a remounted container (E.g.: React replacing the node) resolves to the
1172
+ * fresh element; an element passed directly cannot be re-resolved.
1173
+ */
1174
+ private resolvePreRenderContainerTarget(): HTMLElement {
1175
+ const containerConfig = this.viewConfig.preRenderContainer;
1176
+ let container: Element | null = null;
1177
+ if (typeof containerConfig === 'string') {
1178
+ try {
1179
+ // Resolve against the host's shadow root too, so a selector can
1180
+ // target a container inside the same shadow DOM as the embed —
1181
+ // document.querySelector alone cannot pierce shadow boundaries.
1182
+ container = querySelectorAcrossShadowRoot(containerConfig, this.hostElement);
1183
+ } catch (e) {
1184
+ logger.error(`Invalid CSS selector for preRenderContainer: ${containerConfig}`, e);
1185
+ }
1186
+ } else if (containerConfig) {
1187
+ container = containerConfig;
1188
+ }
1189
+ return (container as HTMLElement) ?? document.body;
1190
+ }
1191
+
1192
+ /**
1193
+ * Makes the container a positioning context for the absolutely positioned
1194
+ * wrapper, stashing the original inline `position` on the element (once) so
1195
+ * destroy() can restore it exactly, leaving no trace. Recording it on the
1196
+ * element rather than per-instance lets the override be reverted even when
1197
+ * embeds share the same container.
1198
+ */
1199
+ private applyPreRenderContainerPositioning(container: HTMLElement): void {
1200
+ if (container === document.body) {
1201
+ return;
1202
+ }
1203
+ const pos = window.getComputedStyle(container).position;
1204
+ if (pos === 'static') {
1205
+ if (container.dataset[PRERENDER_CONTAINER_ORIGINAL_POSITION_KEY] === undefined) {
1206
+ container.dataset[
1207
+ PRERENDER_CONTAINER_ORIGINAL_POSITION_KEY
1208
+ ] = container.style.position;
1209
+ }
1210
+ container.style.position = 'relative';
1211
+ }
1212
+ }
1213
+
1214
+ /**
1215
+ * Re-attaches the wrapper to a live container when the previously resolved
1216
+ * one has been detached or no longer holds the wrapper — e.g. the host app
1217
+ * remounted a custom preRenderContainer, which would otherwise leave a stale
1218
+ * reference and collapse the wrapper. Only string selectors can be
1219
+ * re-resolved; a container passed as an element is left untouched.
1220
+ */
1221
+ private reconcilePreRenderContainer(): void {
1222
+ const wrapper = this.preRenderWrapper;
1223
+ const stored = this.preRenderContainerEl;
1224
+ // Nothing to reconcile until this instance resolved its container.
1225
+ if (!wrapper || !stored) {
1226
+ return;
1227
+ }
1228
+ const storedIsLive = stored === document.body || document.contains(stored);
1229
+ if (storedIsLive && stored.contains(wrapper)) {
1230
+ return;
1231
+ }
1232
+ const resolved = this.resolvePreRenderContainerTarget();
1233
+ // Re-resolution yielded the same (still stale) element — nothing we can
1234
+ // do, e.g. a detached container passed as an HTMLElement.
1235
+ if (resolved === stored && stored.contains(wrapper)) {
1236
+ return;
1237
+ }
1238
+ if (this.containerScrollListener && stored !== resolved) {
1239
+ if (stored !== document.body) {
1240
+ stored.removeEventListener('scroll', this.containerScrollListener);
1241
+ }
1242
+ if (resolved !== document.body) {
1243
+ resolved.addEventListener('scroll', this.containerScrollListener);
1244
+ }
1245
+ }
1246
+ this.applyPreRenderContainerPositioning(resolved);
1247
+ if (wrapper.parentNode !== resolved) {
1248
+ resolved.appendChild(wrapper);
1249
+ }
1250
+ this.preRenderContainerEl = resolved;
1251
+ }
1252
+
1155
1253
  protected insertIntoDOMForPreRender(child: string | Node): void {
1156
1254
  const preRenderChild = this.createPreRenderChild(child);
1157
1255
  const preRenderWrapper = this.createPreRenderWrapper();
@@ -1175,7 +1273,10 @@ export class TsEmbed {
1175
1273
  this.hidePreRender();
1176
1274
  }
1177
1275
 
1178
- document.body.appendChild(preRenderWrapper);
1276
+ const targetContainer = this.resolvePreRenderContainerTarget();
1277
+ this.preRenderContainerEl = targetContainer;
1278
+ this.applyPreRenderContainerPositioning(targetContainer);
1279
+ targetContainer.appendChild(preRenderWrapper);
1179
1280
  }
1180
1281
 
1181
1282
  private showPreRenderByDefault = false;
@@ -1735,6 +1836,55 @@ export class TsEmbed {
1735
1836
  return tsParams;
1736
1837
  }
1737
1838
 
1839
+ /**
1840
+ * Reverts the custom preRenderContainer's `position` to the value it had
1841
+ * before we overrode it to `relative` (see insertIntoDOMForPreRender).
1842
+ *
1843
+ * We restore the original inline value rather than forcing `static`, and we
1844
+ * skip the restore if another preRender wrapper is still mounted inside the
1845
+ * same container — a shared container still needs the positioning context.
1846
+ */
1847
+ private restorePreRenderContainerPosition(): void {
1848
+ const container = this.preRenderContainerEl as HTMLElement | null;
1849
+ if (!container || container === document.body) {
1850
+ return;
1851
+ }
1852
+ // Drop our reference up front so a destroyed embed never pins a
1853
+ // detached container in memory; restoration uses the local handle.
1854
+ this.preRenderContainerEl = null;
1855
+ const originalPosition = container.dataset[PRERENDER_CONTAINER_ORIGINAL_POSITION_KEY];
1856
+ if (originalPosition === undefined) {
1857
+ // We never overrode this container's position; nothing to restore.
1858
+ return;
1859
+ }
1860
+ // This instance's own wrapper has already been removed by now, so any
1861
+ // match here belongs to another embed still sharing the container — it
1862
+ // continues to rely on the positioning context, so leave it in place.
1863
+ const hasOtherWrapper = container.querySelector(
1864
+ `[id^="${PRERENDER_WRAPPER_ID_PREFIX}"]`,
1865
+ );
1866
+ if (hasOtherWrapper) {
1867
+ return;
1868
+ }
1869
+ container.style.position = originalPosition;
1870
+ delete container.dataset[PRERENDER_CONTAINER_ORIGINAL_POSITION_KEY];
1871
+ }
1872
+
1873
+ /**
1874
+ * Detaches and clears the container scroll listener, if one is attached.
1875
+ */
1876
+ private removeContainerScrollListener(): void {
1877
+ if (!this.containerScrollListener) {
1878
+ return;
1879
+ }
1880
+ const customContainer =
1881
+ this.preRenderContainerEl && this.preRenderContainerEl !== document.body
1882
+ ? this.preRenderContainerEl
1883
+ : null;
1884
+ customContainer?.removeEventListener('scroll', this.containerScrollListener);
1885
+ this.containerScrollListener = null;
1886
+ }
1887
+
1738
1888
  /**
1739
1889
  * Destroys the ThoughtSpot embed, and remove any nodes from the DOM.
1740
1890
  * @version SDK: 1.19.1 | ThoughtSpot: *
@@ -1742,8 +1892,10 @@ export class TsEmbed {
1742
1892
  public destroy(): void {
1743
1893
  try {
1744
1894
  this.removeFullscreenChangeHandler();
1895
+ this.removeContainerScrollListener();
1745
1896
  this.unsubscribeToEvents();
1746
1897
  this.preRenderWrapper?.remove();
1898
+ this.restorePreRenderContainerPosition();
1747
1899
  if (!this.isRendered) {
1748
1900
  return;
1749
1901
  }
@@ -1856,15 +2008,21 @@ export class TsEmbed {
1856
2008
 
1857
2009
  this.syncPreRenderStyle();
1858
2010
 
2011
+ const customContainer =
2012
+ this.preRenderContainerEl && this.preRenderContainerEl !== document.body
2013
+ ? this.preRenderContainerEl
2014
+ : null;
2015
+ if (customContainer && !this.containerScrollListener) {
2016
+ this.containerScrollListener = () => this.syncPreRenderStyle();
2017
+ customContainer.addEventListener('scroll', this.containerScrollListener);
2018
+ }
2019
+
1859
2020
  if (!this.viewConfig.doNotTrackPreRenderSize) {
1860
2021
  const observeTarget = (this.insertedDomEl as HTMLElement) ?? this.hostElement;
1861
2022
  this.resizeObserver = new ResizeObserver((entries) => {
1862
2023
  entries.forEach((entry) => {
1863
- if (entry.contentRect && entry.target === observeTarget) {
1864
- setStyleProperties(this.preRenderWrapper, {
1865
- width: `${entry.contentRect.width}px`,
1866
- height: `${entry.contentRect.height}px`,
1867
- });
2024
+ if (entry.target === observeTarget) {
2025
+ this.syncPreRenderStyle();
1868
2026
  }
1869
2027
  });
1870
2028
  });
@@ -1905,11 +2063,22 @@ export class TsEmbed {
1905
2063
  logger.error(ERROR_MESSAGE.SYNC_STYLE_CALLED_BEFORE_RENDER);
1906
2064
  return;
1907
2065
  }
2066
+ // Self-heal if the resolved container was remounted/detached, so we
2067
+ // never measure a stale node (which would collapse the wrapper).
2068
+ this.reconcilePreRenderContainer();
1908
2069
  const elBoundingClient = this.getPreRenderPlaceHolderElement().getBoundingClientRect();
1909
2070
 
2071
+ const containerEl =
2072
+ this.preRenderContainerEl && this.preRenderContainerEl !== document.body
2073
+ ? this.preRenderContainerEl
2074
+ : null;
2075
+ const containerRect = containerEl?.getBoundingClientRect() ?? { x: 0, y: 0 };
2076
+ const scrollX = containerEl ? containerEl.scrollLeft : window.scrollX;
2077
+ const scrollY = containerEl ? containerEl.scrollTop : window.scrollY;
2078
+
1910
2079
  setStyleProperties(this.preRenderWrapper, {
1911
- top: `${elBoundingClient.y + window.scrollY}px`,
1912
- left: `${elBoundingClient.x + window.scrollX}px`,
2080
+ top: `${elBoundingClient.y - containerRect.y + scrollY}px`,
2081
+ left: `${elBoundingClient.x - containerRect.x + scrollX}px`,
1913
2082
  width: `${elBoundingClient.width}px`,
1914
2083
  height: `${elBoundingClient.height}px`,
1915
2084
  position: 'absolute',
@@ -1938,6 +2107,8 @@ export class TsEmbed {
1938
2107
  };
1939
2108
  setStyleProperties(this.preRenderWrapper, preRenderHideStyles);
1940
2109
 
2110
+ this.removeContainerScrollListener();
2111
+
1941
2112
  if (this.resizeObserver) {
1942
2113
  this.resizeObserver.disconnect();
1943
2114
  }
@@ -1959,7 +2130,7 @@ export class TsEmbed {
1959
2130
  */
1960
2131
  public getPreRenderIds() {
1961
2132
  return {
1962
- wrapper: `tsEmbed-pre-render-wrapper-${this.viewConfig.preRenderId}`,
2133
+ wrapper: `${PRERENDER_WRAPPER_ID_PREFIX}${this.viewConfig.preRenderId}`,
1963
2134
  child: `tsEmbed-pre-render-child-${this.viewConfig.preRenderId}`,
1964
2135
  placeHolder: `tsEmbed-pre-render-placeholder-${this.viewConfig.preRenderId}`,
1965
2136
  };
package/src/index.ts CHANGED
@@ -33,7 +33,7 @@ import { SearchEmbed, SearchViewConfig } from './embed/search';
33
33
  import { SearchBarEmbed, SearchBarViewConfig } from './embed/search-bar';
34
34
  import { SpotterAgentEmbed, SpotterAgentEmbedViewConfig, BodylessConversation, BodylessConversationViewConfig} from './embed/bodyless-conversation';
35
35
  import { SpotterEmbed, SpotterEmbedViewConfig, SpotterChatViewConfig, SpotterSidebarViewConfig, ConversationEmbed, ConversationViewConfig } from './embed/conversation';
36
- import { SpotterVizConfig, SpotterVizStarterPrompt } from './embed/spotter-viz-utils';
36
+ import { SpotterVizConfig, SpotterVizStarterPrompt, SpotterVizLoaderTip } from './embed/spotter-viz-utils';
37
37
  import {
38
38
  AuthFailureType, AuthStatus, AuthEvent, AuthEventEmitter,
39
39
  } from './auth';
@@ -143,6 +143,7 @@ export {
143
143
  LiveboardViewConfig,
144
144
  SpotterVizConfig,
145
145
  SpotterVizStarterPrompt,
146
+ SpotterVizLoaderTip,
146
147
  AppViewConfig,
147
148
  PrefetchFeatures,
148
149
  FrameParams,