@webitel/ui-sdk 26.6.32 → 26.6.33

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webitel/ui-sdk",
3
- "version": "26.6.32",
3
+ "version": "26.6.33",
4
4
  "private": false,
5
5
  "scripts": {
6
6
  "dev": "npm run docs:dev",
@@ -1,7 +1,7 @@
1
1
  export const toArray = (value) =>
2
2
  Array.isArray(value)
3
3
  ? value
4
- : !!value
4
+ : value
5
5
  ? [
6
6
  value,
7
7
  ]
@@ -1,4 +1,4 @@
1
- import { computed, onUnmounted, ref } from 'vue';
1
+ import { computed, onBeforeUnmount, ref } from 'vue';
2
2
 
3
3
  import type { DocumentPiPWindow, MediaSnapshot } from '../../types/types';
4
4
 
@@ -21,6 +21,25 @@ export function useDocumentPiP(
21
21
  let originalParent: Node | null = null;
22
22
  let originalNextSibling: Node | null = null;
23
23
 
24
+ /**
25
+ * @author @Oleksandr Palonnyi
26
+ *
27
+ * [WTEL-9774](https://webitel.atlassian.net/browse/WTEL-9774)
28
+ *
29
+ * True while `api.requestWindow()` is in flight.
30
+ * Prevents a second `enterPiP` from starting while the first async
31
+ * request is still pending.
32
+ */
33
+ let isRequestWindowPending = false;
34
+
35
+ /**
36
+ * Set in `onBeforeUnmount`. Used as the abort sentinel for the
37
+ * post-`requestWindow` guard instead of checking `!originalParent`,
38
+ * because a legitimately detached-but-in-document element also has
39
+ * `parentNode === null` and must not be treated as an unmount signal.
40
+ */
41
+ let isUnmounting = false;
42
+
24
43
  const mediaSnapshot: MediaSnapshot[] = [];
25
44
  const { retryPlayback, stopPlaybackRetry } =
26
45
  createPlaybackRetry(mediaSnapshot);
@@ -42,18 +61,12 @@ export function useDocumentPiP(
42
61
  };
43
62
 
44
63
  const restoreElement = () => {
45
- if (!movedEl || !originalParent) {
64
+ if (!movedEl) {
46
65
  mediaSnapshot.splice(0);
47
- movedEl = null;
48
- originalParent = null;
49
- originalNextSibling = null;
50
66
  return;
51
67
  }
52
68
 
53
- if (
54
- originalNextSibling &&
55
- originalNextSibling.parentNode === originalParent
56
- ) {
69
+ if (originalParent) {
57
70
  originalParent.insertBefore(movedEl, originalNextSibling);
58
71
  }
59
72
 
@@ -61,8 +74,6 @@ export function useDocumentPiP(
61
74
  resumePlayback(movedEl, mediaSnapshot);
62
75
 
63
76
  movedEl = null;
64
- originalParent = null;
65
- originalNextSibling = null;
66
77
  };
67
78
 
68
79
  const onPiPWindowClose = () => {
@@ -74,13 +85,36 @@ export function useDocumentPiP(
74
85
  };
75
86
 
76
87
  const enterPiP = async (width = 480, height = 320) => {
77
- if (!isSupported.value || isPiP.value) return;
88
+ if (!isSupported.value || isPiP.value || isRequestWindowPending) return;
78
89
 
79
90
  const el = getElement();
80
91
  if (!el) return;
81
92
 
93
+ /**
94
+ * @author @Oleksandr Palonnyi
95
+ *
96
+ * [WTEL-9774](https://webitel.atlassian.net/browse/WTEL-9774)
97
+ *
98
+ * If the element is stranded inside a closed Document PiP window (e.g. when
99
+ * the consuming app renders the component into a PiP context), rescue it back
100
+ * to the main document before proceeding. Using `document.body` as a temporary
101
+ * parent is safe: the element is `position:fixed` so its visual position is
102
+ * viewport-relative regardless of where it sits in the DOM tree, and Vue will
103
+ * remove it from `document.body` when the component eventually unmounts.
104
+ */
105
+ if (el.ownerDocument !== document) {
106
+ document.body.appendChild(el);
107
+ }
108
+
109
+ if (!originalParent) {
110
+ originalParent = el.parentNode;
111
+ originalNextSibling = el.nextSibling;
112
+ }
113
+
82
114
  const api = (window as DocumentPiPWindow).documentPictureInPicture;
83
115
  let win: Window;
116
+
117
+ isRequestWindowPending = true;
84
118
  try {
85
119
  win = await api.requestWindow({
86
120
  width,
@@ -88,6 +122,24 @@ export function useDocumentPiP(
88
122
  });
89
123
  } catch {
90
124
  return;
125
+ } finally {
126
+ isRequestWindowPending = false;
127
+ }
128
+
129
+ /**
130
+ * @author @Oleksandr Palonnyi
131
+ *
132
+ * [WTEL-9774](https://webitel.atlassian.net/browse/WTEL-9774)
133
+ *
134
+ * `onBeforeUnmount` can fire while `requestWindow` is awaited above.
135
+ * Abort if that happened so we don't open a PiP window that can never
136
+ * restore its element. We use `isUnmounting` rather than `!originalParent`
137
+ * because a legitimately detached element also has `parentNode === null`
138
+ * and must not be treated as an unmount signal.
139
+ */
140
+ if (isUnmounting) {
141
+ win.close();
142
+ return;
91
143
  }
92
144
 
93
145
  pipWindow = win;
@@ -97,10 +149,7 @@ export function useDocumentPiP(
97
149
  'margin:0;overflow:hidden;width:100%;height:100%;';
98
150
  bridgeCustomElements(el, win);
99
151
 
100
- originalParent = el.parentNode;
101
- originalNextSibling = el.nextSibling;
102
152
  movedEl = el;
103
-
104
153
  snapshotMedia(el, mediaSnapshot);
105
154
  win.document.body.appendChild(el);
106
155
 
@@ -131,8 +180,11 @@ export function useDocumentPiP(
131
180
  isPiP.value = false;
132
181
  };
133
182
 
134
- onUnmounted(() => {
183
+ onBeforeUnmount(() => {
184
+ isUnmounting = true;
135
185
  if (isPiP.value) exitPiP();
186
+ originalParent = null;
187
+ originalNextSibling = null;
136
188
  });
137
189
 
138
190
  return {