@pip-it-up/react 0.1.1 → 0.1.5
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/README.md +60 -12
- package/dist/index.d.mts +36 -4
- package/dist/index.d.ts +36 -4
- package/dist/index.js +236 -82
- package/dist/index.mjs +208 -54
- package/package.json +17 -4
package/README.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# @pip-it-up/react
|
|
2
2
|
|
|
3
|
-
React bindings for
|
|
3
|
+
React bindings for `pip-it-up` — the **Document Picture-in-Picture** engine.
|
|
4
|
+
|
|
5
|
+
## What is Document Picture-in-Picture?
|
|
6
|
+
|
|
7
|
+
The [Document Picture-in-Picture API](https://developer.mozilla.org/en-US/docs/Web/API/Document_Picture-in-Picture_API) is a new browser capability that allows you to open a floating window that can be populated with any arbitrary HTML content, rather than just a video element.
|
|
8
|
+
|
|
9
|
+
`@pip-it-up/react` makes it trivial to use this API in React applications with familiar patterns like Portals, Hooks, and Controlled Components.
|
|
4
10
|
|
|
5
11
|
## Installation
|
|
6
12
|
|
|
@@ -12,7 +18,7 @@ npm install @pip-it-up/react @pip-it-up/core
|
|
|
12
18
|
|
|
13
19
|
### `<PipWrapper>`
|
|
14
20
|
|
|
15
|
-
Wraps the content you want to move into the
|
|
21
|
+
Wraps the content you want to move into the **Picture-in-Picture** window.
|
|
16
22
|
|
|
17
23
|
#### Uncontrolled (Default)
|
|
18
24
|
```tsx
|
|
@@ -32,34 +38,76 @@ const [isOpen, setIsOpen] = useState(false);
|
|
|
32
38
|
```
|
|
33
39
|
|
|
34
40
|
#### Props
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
- `
|
|
41
|
+
|
|
42
|
+
Supports all `PipOptions` from `@pip-it-up/core`, including:
|
|
43
|
+
|
|
44
|
+
- **`width` / `height`** (number, optional): If provided, forces the PiP window to these dimensions. **If omitted, the library uses a `ResizeObserver` to automatically match the component's exact size on the page.**
|
|
45
|
+
- **`mode`** (`"move" | "portal"`, default: `"move"`):
|
|
46
|
+
- In this React package, both `"move"` and `"portal"` utilize **React Portals** to move content safely between windows. They behave identically: a placeholder is left in the original spot, and the component stays in the same React tree.
|
|
47
|
+
- **`copyStyles`** (`"sync" | "once" | false`, default: `"sync"`):
|
|
48
|
+
- `"sync"`: Real-time synchronization of CSS changes (MutationObserver).
|
|
49
|
+
- `"once"`: One-time copy at window open.
|
|
50
|
+
- **`reserveSpace`** (boolean, default: `true`): Whether to show a placeholder in the original position to prevent layout jumps.
|
|
51
|
+
- **`placeholder`** (ReactNode): Custom component to show in the placeholder area.
|
|
52
|
+
- **`centerInPip`** (boolean, default: `false`): Automatically centers your content in the PiP window.
|
|
53
|
+
- **`open`** (boolean): Controlled state for the window.
|
|
54
|
+
- **`onOpenChange`** (callback): Fired when the window opens or closes.
|
|
39
55
|
|
|
40
56
|
### `<PipTrigger>`
|
|
41
57
|
|
|
42
|
-
A button that toggles the
|
|
58
|
+
A button that toggles the **Picture-in-Picture** window.
|
|
43
59
|
|
|
44
60
|
```tsx
|
|
45
61
|
<PipTrigger asChild>
|
|
46
|
-
<button className="my-custom-btn">Open
|
|
62
|
+
<button className="my-custom-btn">Open Picture-in-Picture</button>
|
|
47
63
|
</PipTrigger>
|
|
48
64
|
```
|
|
49
65
|
|
|
50
66
|
## Hooks
|
|
51
67
|
|
|
52
68
|
### `usePip()`
|
|
53
|
-
Returns the context state.
|
|
69
|
+
Returns the context state for managing the **Picture-in-Picture** lifecycle.
|
|
54
70
|
```tsx
|
|
55
|
-
const { isOpen, pipWindow, instance } = usePip();
|
|
71
|
+
const { isOpen, pipWindow, instance, isInsidePip } = usePip();
|
|
56
72
|
```
|
|
73
|
+
- **`isOpen`**: Boolean indicating if the PiP window is open.
|
|
74
|
+
- **`pipWindow`**: The native `Window` object of the PiP instance (null if closed).
|
|
75
|
+
- **`isInsidePip`**: Boolean that is `true` only when the component is being rendered inside the PiP window.
|
|
76
|
+
- **`instance`**: The underlying `@pip-it-up/core` instance.
|
|
77
|
+
|
|
78
|
+
> [!NOTE]
|
|
79
|
+
> `usePipContext()` is also available if you only need the raw context without the extra convenience properties of `usePip()`.
|
|
57
80
|
|
|
58
81
|
### `useIsPipSupported()`
|
|
59
|
-
Returns
|
|
82
|
+
Returns `true` if the browser natively supports the **Document Picture-in-Picture API**.
|
|
60
83
|
```tsx
|
|
61
84
|
const isSupported = useIsPipSupported();
|
|
62
85
|
```
|
|
63
86
|
|
|
87
|
+
## Tips & Gotchas
|
|
88
|
+
|
|
89
|
+
### CSS Inheritance
|
|
90
|
+
The Picture-in-Picture window is a separate document. While `@pip-it-up` automatically copies stylesheets and `body`/`html` classes, your content will **not** inherit styles from parent elements outside the `<PipWrapper>` (like a `#root` div or a theme provider).
|
|
91
|
+
* **Fix**: Add necessary alignment or theme classes (e.g., `text-center`, `dark`) directly to the content inside the `<PipWrapper>`.
|
|
92
|
+
|
|
93
|
+
### Complex Editors (Monaco, TipTap, Canvas)
|
|
94
|
+
Editors that bind to the `document` object at initialization may break when moved to a new window.
|
|
95
|
+
* **Fix 1**: Force a remount of the editor when moving to PiP by using the `isInsidePip` state as a React `key`.
|
|
96
|
+
* **Fix 2**: Lift your editor state (content, cursors, etc.) **above** the `<PipWrapper>`. Since the wrapper unmounts and remounts its children when moving them into the PiP window, any state stored *inside* the wrapper will be lost during the transition.
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
const [content, setContent] = useState("");
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<PipWrapper>
|
|
103
|
+
<RichTextEditor
|
|
104
|
+
key={isInsidePip ? 'pip' : 'main'}
|
|
105
|
+
value={content}
|
|
106
|
+
onChange={setContent}
|
|
107
|
+
/>
|
|
108
|
+
</PipWrapper>
|
|
109
|
+
);
|
|
110
|
+
```
|
|
111
|
+
|
|
64
112
|
## Next.js / SSR
|
|
65
|
-
Because the Document
|
|
113
|
+
Because the **Document Picture-in-Picture API** is browser-only, ensure components interacting with it are rendered on the client (`"use client"`).
|
package/dist/index.d.mts
CHANGED
|
@@ -2,14 +2,44 @@ import * as React from 'react';
|
|
|
2
2
|
import React__default, { ElementType, ReactNode } from 'react';
|
|
3
3
|
import { PipOptions, PipInstance, PipState } from '@pip-it-up/core';
|
|
4
4
|
|
|
5
|
-
interface PipWrapperProps extends PipOptions {
|
|
5
|
+
interface PipWrapperProps extends Omit<PipOptions, 'mode'> {
|
|
6
6
|
open?: boolean;
|
|
7
7
|
defaultOpen?: boolean;
|
|
8
8
|
onOpenChange?: (open: boolean) => void;
|
|
9
9
|
as?: ElementType;
|
|
10
10
|
originAs?: ElementType;
|
|
11
11
|
children?: ReactNode;
|
|
12
|
+
placeholder?: ReactNode;
|
|
13
|
+
placeholderClassName?: string;
|
|
14
|
+
/**
|
|
15
|
+
* @deprecated `mode` is not supported in PipWrapper. The React package always
|
|
16
|
+
* uses portal mode internally because React manages its own DOM — vanilla
|
|
17
|
+
* `move`/`clone` modes would conflict with React's reconciler. For clone mode,
|
|
18
|
+
* use the vanilla `createPip()` API directly.
|
|
19
|
+
*/
|
|
20
|
+
mode?: PipOptions['mode'];
|
|
12
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* A React wrapper for the Document Picture-in-Picture API.
|
|
24
|
+
*
|
|
25
|
+
* **Important**: The underlying core instance always uses `mode: 'portal'`
|
|
26
|
+
* regardless of any `mode` prop. This is because React handles DOM movement
|
|
27
|
+
* via its own Portal system — using the core's vanilla DOM `move`/`clone`
|
|
28
|
+
* modes would cause `removeChild` errors and reconciliation crashes.
|
|
29
|
+
*
|
|
30
|
+
* **Implication for vanilla interop**: If you register a `<PipWrapper id="foo">`
|
|
31
|
+
* and then call `getPip('foo').open({ contentEl, originEl })` from vanilla JS,
|
|
32
|
+
* the core runs in portal mode — meaning `applyMoveMode` and `applyCloneMode`
|
|
33
|
+
* are skipped. The vanilla caller would get an empty PiP window. For vanilla
|
|
34
|
+
* DOM manipulation, create a separate instance with `createPip()` directly.
|
|
35
|
+
*
|
|
36
|
+
* **Layout note**: The outer wrapper element (`originAs`, defaults to `div`)
|
|
37
|
+
* uses `display: contents` to avoid adding an extra layout box. This means:
|
|
38
|
+
* - `getBoundingClientRect()` on the forwarded ref will return all zeros.
|
|
39
|
+
* - Screen readers may skip the element.
|
|
40
|
+
* If you need to measure the wrapper, measure the inner content element
|
|
41
|
+
* (the `as` element) instead.
|
|
42
|
+
*/
|
|
13
43
|
declare const PipWrapper: React__default.ForwardRefExoticComponent<PipWrapperProps & React__default.RefAttributes<HTMLElement>>;
|
|
14
44
|
|
|
15
45
|
interface PipTriggerProps extends React__default.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
@@ -20,6 +50,7 @@ interface PipTriggerProps extends React__default.ButtonHTMLAttributes<HTMLButton
|
|
|
20
50
|
renderOpen?: React__default.ReactNode;
|
|
21
51
|
renderClose?: React__default.ReactNode;
|
|
22
52
|
renderUnsupported?: React__default.ReactNode | null;
|
|
53
|
+
hideInPip?: boolean;
|
|
23
54
|
}
|
|
24
55
|
declare const PipTrigger: React__default.ForwardRefExoticComponent<PipTriggerProps & React__default.RefAttributes<HTMLElement>>;
|
|
25
56
|
|
|
@@ -34,13 +65,14 @@ declare function usePip<T extends HTMLElement = HTMLDivElement>(options?: PipOpt
|
|
|
34
65
|
pipWindow: Window | null;
|
|
35
66
|
};
|
|
36
67
|
|
|
37
|
-
interface PipContextValue
|
|
68
|
+
interface PipContextValue {
|
|
38
69
|
instance: PipInstance;
|
|
39
70
|
state: PipState;
|
|
71
|
+
isInsidePip: boolean;
|
|
40
72
|
}
|
|
41
|
-
declare const PipContext: React.Context<PipContextValue
|
|
73
|
+
declare const PipContext: React.Context<PipContextValue | null>;
|
|
42
74
|
|
|
43
|
-
declare function usePipContext
|
|
75
|
+
declare function usePipContext(): PipContextValue;
|
|
44
76
|
|
|
45
77
|
declare const useIsPipSupported: () => boolean;
|
|
46
78
|
|
package/dist/index.d.ts
CHANGED
|
@@ -2,14 +2,44 @@ import * as React from 'react';
|
|
|
2
2
|
import React__default, { ElementType, ReactNode } from 'react';
|
|
3
3
|
import { PipOptions, PipInstance, PipState } from '@pip-it-up/core';
|
|
4
4
|
|
|
5
|
-
interface PipWrapperProps extends PipOptions {
|
|
5
|
+
interface PipWrapperProps extends Omit<PipOptions, 'mode'> {
|
|
6
6
|
open?: boolean;
|
|
7
7
|
defaultOpen?: boolean;
|
|
8
8
|
onOpenChange?: (open: boolean) => void;
|
|
9
9
|
as?: ElementType;
|
|
10
10
|
originAs?: ElementType;
|
|
11
11
|
children?: ReactNode;
|
|
12
|
+
placeholder?: ReactNode;
|
|
13
|
+
placeholderClassName?: string;
|
|
14
|
+
/**
|
|
15
|
+
* @deprecated `mode` is not supported in PipWrapper. The React package always
|
|
16
|
+
* uses portal mode internally because React manages its own DOM — vanilla
|
|
17
|
+
* `move`/`clone` modes would conflict with React's reconciler. For clone mode,
|
|
18
|
+
* use the vanilla `createPip()` API directly.
|
|
19
|
+
*/
|
|
20
|
+
mode?: PipOptions['mode'];
|
|
12
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* A React wrapper for the Document Picture-in-Picture API.
|
|
24
|
+
*
|
|
25
|
+
* **Important**: The underlying core instance always uses `mode: 'portal'`
|
|
26
|
+
* regardless of any `mode` prop. This is because React handles DOM movement
|
|
27
|
+
* via its own Portal system — using the core's vanilla DOM `move`/`clone`
|
|
28
|
+
* modes would cause `removeChild` errors and reconciliation crashes.
|
|
29
|
+
*
|
|
30
|
+
* **Implication for vanilla interop**: If you register a `<PipWrapper id="foo">`
|
|
31
|
+
* and then call `getPip('foo').open({ contentEl, originEl })` from vanilla JS,
|
|
32
|
+
* the core runs in portal mode — meaning `applyMoveMode` and `applyCloneMode`
|
|
33
|
+
* are skipped. The vanilla caller would get an empty PiP window. For vanilla
|
|
34
|
+
* DOM manipulation, create a separate instance with `createPip()` directly.
|
|
35
|
+
*
|
|
36
|
+
* **Layout note**: The outer wrapper element (`originAs`, defaults to `div`)
|
|
37
|
+
* uses `display: contents` to avoid adding an extra layout box. This means:
|
|
38
|
+
* - `getBoundingClientRect()` on the forwarded ref will return all zeros.
|
|
39
|
+
* - Screen readers may skip the element.
|
|
40
|
+
* If you need to measure the wrapper, measure the inner content element
|
|
41
|
+
* (the `as` element) instead.
|
|
42
|
+
*/
|
|
13
43
|
declare const PipWrapper: React__default.ForwardRefExoticComponent<PipWrapperProps & React__default.RefAttributes<HTMLElement>>;
|
|
14
44
|
|
|
15
45
|
interface PipTriggerProps extends React__default.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
@@ -20,6 +50,7 @@ interface PipTriggerProps extends React__default.ButtonHTMLAttributes<HTMLButton
|
|
|
20
50
|
renderOpen?: React__default.ReactNode;
|
|
21
51
|
renderClose?: React__default.ReactNode;
|
|
22
52
|
renderUnsupported?: React__default.ReactNode | null;
|
|
53
|
+
hideInPip?: boolean;
|
|
23
54
|
}
|
|
24
55
|
declare const PipTrigger: React__default.ForwardRefExoticComponent<PipTriggerProps & React__default.RefAttributes<HTMLElement>>;
|
|
25
56
|
|
|
@@ -34,13 +65,14 @@ declare function usePip<T extends HTMLElement = HTMLDivElement>(options?: PipOpt
|
|
|
34
65
|
pipWindow: Window | null;
|
|
35
66
|
};
|
|
36
67
|
|
|
37
|
-
interface PipContextValue
|
|
68
|
+
interface PipContextValue {
|
|
38
69
|
instance: PipInstance;
|
|
39
70
|
state: PipState;
|
|
71
|
+
isInsidePip: boolean;
|
|
40
72
|
}
|
|
41
|
-
declare const PipContext: React.Context<PipContextValue
|
|
73
|
+
declare const PipContext: React.Context<PipContextValue | null>;
|
|
42
74
|
|
|
43
|
-
declare function usePipContext
|
|
75
|
+
declare function usePipContext(): PipContextValue;
|
|
44
76
|
|
|
45
77
|
declare const useIsPipSupported: () => boolean;
|
|
46
78
|
|
package/dist/index.js
CHANGED
|
@@ -40,7 +40,7 @@ __export(index_exports, {
|
|
|
40
40
|
module.exports = __toCommonJS(index_exports);
|
|
41
41
|
|
|
42
42
|
// src/PipWrapper.tsx
|
|
43
|
-
var
|
|
43
|
+
var import_react3 = require("react");
|
|
44
44
|
var import_core = require("@pip-it-up/core");
|
|
45
45
|
|
|
46
46
|
// src/PipContext.tsx
|
|
@@ -49,15 +49,32 @@ var PipContext = (0, import_react.createContext)(null);
|
|
|
49
49
|
|
|
50
50
|
// src/PipPortal.tsx
|
|
51
51
|
var import_react_dom = require("react-dom");
|
|
52
|
+
|
|
53
|
+
// src/usePipContext.ts
|
|
54
|
+
var import_react2 = require("react");
|
|
55
|
+
function usePipContext() {
|
|
56
|
+
const context = (0, import_react2.useContext)(PipContext);
|
|
57
|
+
if (!context) {
|
|
58
|
+
throw new Error("usePipContext must be used within a <PipWrapper>");
|
|
59
|
+
}
|
|
60
|
+
return context;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/PipPortal.tsx
|
|
64
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
52
65
|
var PipPortal = ({ children, pipWindow }) => {
|
|
53
|
-
|
|
66
|
+
const context = usePipContext();
|
|
67
|
+
return (0, import_react_dom.createPortal)(
|
|
68
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PipContext.Provider, { value: { ...context, isInsidePip: true }, children }),
|
|
69
|
+
pipWindow.document.body
|
|
70
|
+
);
|
|
54
71
|
};
|
|
55
72
|
|
|
56
73
|
// src/PipWrapper.tsx
|
|
57
|
-
var
|
|
74
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
58
75
|
var emptyServerState = { isOpen: false, isSupported: false, pipWindow: null };
|
|
59
76
|
var getServerState = () => emptyServerState;
|
|
60
|
-
var PipWrapper = (0,
|
|
77
|
+
var PipWrapper = (0, import_react3.forwardRef)((props, ref) => {
|
|
61
78
|
const {
|
|
62
79
|
open: controlledOpen,
|
|
63
80
|
defaultOpen = false,
|
|
@@ -65,35 +82,57 @@ var PipWrapper = (0, import_react2.forwardRef)((props, ref) => {
|
|
|
65
82
|
as: Component = "div",
|
|
66
83
|
originAs: OriginComponent = "div",
|
|
67
84
|
children,
|
|
85
|
+
placeholder,
|
|
86
|
+
placeholderClassName,
|
|
68
87
|
...coreOptions
|
|
69
88
|
} = props;
|
|
70
|
-
const contentRef = (0,
|
|
71
|
-
const originRef = (0,
|
|
72
|
-
const instanceRef = (0,
|
|
73
|
-
(0,
|
|
89
|
+
const contentRef = (0, import_react3.useRef)(null);
|
|
90
|
+
const originRef = (0, import_react3.useRef)(null);
|
|
91
|
+
const instanceRef = (0, import_react3.useRef)(null);
|
|
92
|
+
(0, import_react3.useImperativeHandle)(ref, () => originRef.current);
|
|
74
93
|
if (!instanceRef.current) {
|
|
75
|
-
|
|
94
|
+
const { id: _id, mode: _mode, ...factoryOptions } = coreOptions;
|
|
95
|
+
if (_mode === "clone") {
|
|
96
|
+
console.warn(
|
|
97
|
+
'[PipWrapper] mode="clone" is not supported in the React package. PipWrapper always uses portal mode internally. For clone mode, use the vanilla createPip() API directly. Falling back to portal mode.'
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
instanceRef.current = (0, import_core.createPip)({ ...factoryOptions, mode: "portal" });
|
|
76
101
|
}
|
|
77
102
|
const instance = instanceRef.current;
|
|
78
|
-
(0,
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
103
|
+
(0, import_react3.useEffect)(() => {
|
|
104
|
+
if (coreOptions.id) {
|
|
105
|
+
(0, import_core.registerPip)(coreOptions.id, instance);
|
|
106
|
+
return () => {
|
|
107
|
+
(0, import_core.unregisterPip)(coreOptions.id);
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}, [coreOptions.id, instance]);
|
|
111
|
+
(0, import_react3.useEffect)(() => {
|
|
85
112
|
return () => {
|
|
86
|
-
|
|
113
|
+
if (instanceRef.current) {
|
|
114
|
+
instanceRef.current.destroy();
|
|
115
|
+
}
|
|
87
116
|
};
|
|
88
|
-
}, [
|
|
89
|
-
const state = (0,
|
|
117
|
+
}, []);
|
|
118
|
+
const state = (0, import_react3.useSyncExternalStore)(
|
|
90
119
|
instance.subscribe,
|
|
91
120
|
instance.getState,
|
|
92
121
|
getServerState
|
|
93
122
|
);
|
|
123
|
+
(0, import_react3.useLayoutEffect)(() => {
|
|
124
|
+
if (typeof instance.setDefaultElements === "function") {
|
|
125
|
+
instance.setDefaultElements({
|
|
126
|
+
contentEl: contentRef.current || void 0,
|
|
127
|
+
originEl: originRef.current || void 0
|
|
128
|
+
});
|
|
129
|
+
} else {
|
|
130
|
+
console.error("[PipWrapper] setDefaultElements is MISSING on instance!");
|
|
131
|
+
}
|
|
132
|
+
}, [instance, state.isOpen]);
|
|
94
133
|
const isControlled = controlledOpen !== void 0;
|
|
95
|
-
const prevOpenRef = (0,
|
|
96
|
-
(0,
|
|
134
|
+
const prevOpenRef = (0, import_react3.useRef)(state.isOpen);
|
|
135
|
+
(0, import_react3.useEffect)(() => {
|
|
97
136
|
if (state.isOpen !== prevOpenRef.current) {
|
|
98
137
|
if (onOpenChange) {
|
|
99
138
|
onOpenChange(state.isOpen);
|
|
@@ -101,48 +140,140 @@ var PipWrapper = (0, import_react2.forwardRef)((props, ref) => {
|
|
|
101
140
|
prevOpenRef.current = state.isOpen;
|
|
102
141
|
}
|
|
103
142
|
}, [state.isOpen, onOpenChange]);
|
|
104
|
-
(0,
|
|
143
|
+
const prevControlledOpenRef = (0, import_react3.useRef)(false);
|
|
144
|
+
(0, import_react3.useEffect)(() => {
|
|
105
145
|
if (isControlled) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
146
|
+
const changedToOpen = controlledOpen && !prevControlledOpenRef.current;
|
|
147
|
+
const changedToClosed = !controlledOpen && prevControlledOpenRef.current;
|
|
148
|
+
if (changedToOpen && !state.isOpen) {
|
|
149
|
+
if (contentRef.current) {
|
|
150
|
+
const rect = contentRef.current.getBoundingClientRect();
|
|
151
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
152
|
+
lastSizeRef.current = { width: rect.width, height: rect.height };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
instance.open().catch((err) => {
|
|
156
|
+
if (err.name === "NotAllowedError") {
|
|
157
|
+
console.warn('[PipWrapper] PiP window opening blocked: requires user activation. Ensure the "open" prop is changed within a user gesture handler.');
|
|
158
|
+
} else {
|
|
159
|
+
console.error("[PipWrapper] Failed to open PiP window", err);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
} else if (changedToClosed && state.isOpen) {
|
|
109
163
|
instance.close();
|
|
110
164
|
}
|
|
165
|
+
prevControlledOpenRef.current = controlledOpen;
|
|
111
166
|
}
|
|
112
167
|
}, [controlledOpen, isControlled, state.isOpen, instance]);
|
|
113
|
-
const defaultOpenHandled = (0,
|
|
114
|
-
(0,
|
|
168
|
+
const defaultOpenHandled = (0, import_react3.useRef)(false);
|
|
169
|
+
(0, import_react3.useEffect)(() => {
|
|
115
170
|
if (!isControlled && defaultOpen && !defaultOpenHandled.current) {
|
|
116
171
|
defaultOpenHandled.current = true;
|
|
117
|
-
instance.open(
|
|
172
|
+
instance.open().catch((err) => {
|
|
173
|
+
if (err.name === "NotAllowedError") {
|
|
174
|
+
console.warn("[PipWrapper] PiP window defaultOpen blocked: requires user activation.");
|
|
175
|
+
} else {
|
|
176
|
+
console.error("[PipWrapper] Failed to open PiP window via defaultOpen", err);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
118
179
|
}
|
|
119
180
|
}, [defaultOpen, isControlled, instance]);
|
|
120
|
-
const mode = coreOptions.mode || "move";
|
|
121
|
-
|
|
181
|
+
const mode = coreOptions.mode === "clone" ? "move" : coreOptions.mode || "move";
|
|
182
|
+
const defaultPlaceholder = /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { padding: "12px", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", backgroundColor: "transparent", border: "1px dashed color-mix(in srgb, currentColor 30%, #ccc)", borderRadius: "inherit", width: "100%", height: "100%", boxSizing: "border-box", overflow: "hidden" }, children: [
|
|
183
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { marginBottom: "4px", fontSize: "0.875rem", fontWeight: 500, opacity: 0.6, textAlign: "center" }, children: "\u{1F4FA} In PiP" }),
|
|
184
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { onClick: () => instance.close(), style: { fontSize: "0.75rem", padding: "4px 8px", cursor: "pointer", borderRadius: "4px", border: "1px solid currentColor", background: "transparent", opacity: 0.6 }, children: "Restore" })
|
|
185
|
+
] });
|
|
186
|
+
const placeholderContent = placeholder !== void 0 ? placeholder : defaultPlaceholder;
|
|
187
|
+
const lastSizeRef = (0, import_react3.useRef)({ width: 0, height: 0 });
|
|
188
|
+
(0, import_react3.useEffect)(() => {
|
|
189
|
+
if (!contentRef.current || state.isOpen) return;
|
|
190
|
+
const observer = new ResizeObserver((entries) => {
|
|
191
|
+
for (const entry of entries) {
|
|
192
|
+
if (entry.contentRect.width > 0 && entry.contentRect.height > 0) {
|
|
193
|
+
lastSizeRef.current = {
|
|
194
|
+
width: entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width,
|
|
195
|
+
height: entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
observer.observe(contentRef.current);
|
|
201
|
+
return () => observer.disconnect();
|
|
202
|
+
}, [state.isOpen]);
|
|
203
|
+
const prevIsOpenRef = (0, import_react3.useRef)(state.isOpen);
|
|
204
|
+
(0, import_react3.useLayoutEffect)(() => {
|
|
205
|
+
if (prevIsOpenRef.current && !state.isOpen && contentRef.current) {
|
|
206
|
+
const rect = contentRef.current.getBoundingClientRect();
|
|
207
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
208
|
+
lastSizeRef.current = { width: rect.width, height: rect.height };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
prevIsOpenRef.current = state.isOpen;
|
|
212
|
+
}, [state.isOpen]);
|
|
213
|
+
(0, import_react3.useLayoutEffect)(() => {
|
|
214
|
+
if ((mode === "move" || mode === "portal") && state.isOpen && coreOptions.reserveSpace !== false && originRef.current) {
|
|
215
|
+
const { width, height } = lastSizeRef.current;
|
|
216
|
+
const origin = originRef.current;
|
|
217
|
+
if (width > 0 && height > 0) {
|
|
218
|
+
origin.style.minWidth = `${width}px`;
|
|
219
|
+
origin.style.minHeight = `${height}px`;
|
|
220
|
+
origin.style.width = `${width}px`;
|
|
221
|
+
origin.style.height = `${height}px`;
|
|
222
|
+
origin.style.display = "inline-block";
|
|
223
|
+
origin.style.verticalAlign = "top";
|
|
224
|
+
return () => {
|
|
225
|
+
origin.style.minWidth = "";
|
|
226
|
+
origin.style.minHeight = "";
|
|
227
|
+
origin.style.width = "";
|
|
228
|
+
origin.style.height = "";
|
|
229
|
+
origin.style.display = "";
|
|
230
|
+
origin.style.verticalAlign = "";
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}, [mode, state.isOpen, coreOptions.reserveSpace, instance]);
|
|
235
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PipContext.Provider, { value: { instance, state, isInsidePip: false }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(OriginComponent, { ref: originRef, style: { display: "contents" }, children: (mode === "portal" || mode === "move") && state.isOpen && state.pipWindow ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
236
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
237
|
+
"div",
|
|
238
|
+
{
|
|
239
|
+
className: placeholderClassName,
|
|
240
|
+
style: {
|
|
241
|
+
width: coreOptions.width ? `${coreOptions.width}px` : "100%",
|
|
242
|
+
height: lastSizeRef.current.height ? `${lastSizeRef.current.height}px` : "auto",
|
|
243
|
+
display: "inline-block",
|
|
244
|
+
verticalAlign: "top",
|
|
245
|
+
boxSizing: "border-box"
|
|
246
|
+
},
|
|
247
|
+
children: placeholderContent
|
|
248
|
+
},
|
|
249
|
+
"placeholder"
|
|
250
|
+
),
|
|
251
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(PipPortal, { pipWindow: state.pipWindow, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Component, { ref: contentRef, children }) })
|
|
252
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Component, { ref: contentRef, children }, "content") }) });
|
|
122
253
|
});
|
|
123
254
|
PipWrapper.displayName = "PipWrapper";
|
|
124
255
|
|
|
125
256
|
// src/PipTrigger.tsx
|
|
126
|
-
var
|
|
257
|
+
var import_react6 = __toESM(require("react"));
|
|
127
258
|
var import_core3 = require("@pip-it-up/core");
|
|
128
259
|
|
|
129
260
|
// src/useIsPipSupported.ts
|
|
130
|
-
var
|
|
261
|
+
var import_react4 = require("react");
|
|
131
262
|
var import_core2 = require("@pip-it-up/core");
|
|
132
263
|
var useIsPipSupported = () => {
|
|
133
|
-
const [supported, setSupported] = (0,
|
|
134
|
-
(0,
|
|
264
|
+
const [supported, setSupported] = (0, import_react4.useState)(false);
|
|
265
|
+
(0, import_react4.useEffect)(() => {
|
|
135
266
|
setSupported((0, import_core2.isSupported)());
|
|
136
267
|
}, []);
|
|
137
268
|
return supported;
|
|
138
269
|
};
|
|
139
270
|
|
|
140
271
|
// src/Slot.tsx
|
|
141
|
-
var
|
|
142
|
-
var Slot = (0,
|
|
272
|
+
var import_react5 = __toESM(require("react"));
|
|
273
|
+
var Slot = (0, import_react5.forwardRef)((props, ref) => {
|
|
143
274
|
const { children, ...slotProps } = props;
|
|
144
|
-
if (
|
|
145
|
-
return
|
|
275
|
+
if (import_react5.default.isValidElement(children)) {
|
|
276
|
+
return import_react5.default.cloneElement(children, {
|
|
146
277
|
...slotProps,
|
|
147
278
|
...children.props,
|
|
148
279
|
style: {
|
|
@@ -153,26 +284,26 @@ var Slot = (0, import_react4.forwardRef)((props, ref) => {
|
|
|
153
284
|
ref: (node) => {
|
|
154
285
|
if (typeof ref === "function") ref(node);
|
|
155
286
|
else if (ref) ref.current = node;
|
|
156
|
-
const childRef = children.ref;
|
|
287
|
+
const childRef = children.props.ref || children.ref;
|
|
157
288
|
if (typeof childRef === "function") childRef(node);
|
|
158
289
|
else if (childRef) childRef.current = node;
|
|
159
290
|
}
|
|
160
291
|
});
|
|
161
292
|
}
|
|
162
|
-
if (
|
|
163
|
-
|
|
293
|
+
if (import_react5.default.Children.count(children) > 1) {
|
|
294
|
+
throw new Error("PipTrigger asChild expects a single React element child");
|
|
164
295
|
}
|
|
165
296
|
return null;
|
|
166
297
|
});
|
|
167
298
|
Slot.displayName = "Slot";
|
|
168
299
|
|
|
169
300
|
// src/PipTrigger.tsx
|
|
170
|
-
var
|
|
301
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
171
302
|
var emptySubscribe = () => () => {
|
|
172
303
|
};
|
|
173
304
|
var emptyServerState2 = { isOpen: false, isSupported: false, pipWindow: null };
|
|
174
305
|
var emptyGetState = () => emptyServerState2;
|
|
175
|
-
var PipTrigger = (0,
|
|
306
|
+
var PipTrigger = (0, import_react6.forwardRef)((props, ref) => {
|
|
176
307
|
const {
|
|
177
308
|
pipId,
|
|
178
309
|
asChild,
|
|
@@ -181,83 +312,116 @@ var PipTrigger = (0, import_react5.forwardRef)((props, ref) => {
|
|
|
181
312
|
renderOpen,
|
|
182
313
|
renderClose,
|
|
183
314
|
renderUnsupported = null,
|
|
315
|
+
hideInPip = true,
|
|
184
316
|
onClick,
|
|
185
317
|
children,
|
|
186
318
|
...rest
|
|
187
319
|
} = props;
|
|
188
320
|
const isSupported2 = useIsPipSupported();
|
|
189
|
-
const context = (0,
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const unsub = (0, import_core3.subscribeRegistry)(pipId, () => {
|
|
195
|
-
setRegistryInstance((0, import_core3.getPip)(pipId));
|
|
196
|
-
});
|
|
197
|
-
return unsub;
|
|
198
|
-
}
|
|
321
|
+
const context = (0, import_react6.useContext)(PipContext);
|
|
322
|
+
const registrySubscribe = import_react6.default.useCallback((callback) => {
|
|
323
|
+
if (!pipId) return () => {
|
|
324
|
+
};
|
|
325
|
+
return (0, import_core3.subscribeRegistry)(pipId, callback);
|
|
199
326
|
}, [pipId]);
|
|
327
|
+
const getRegistrySnapshot = import_react6.default.useCallback(() => {
|
|
328
|
+
return pipId ? (0, import_core3.getPip)(pipId) : null;
|
|
329
|
+
}, [pipId]);
|
|
330
|
+
const registryInstance = (0, import_react6.useSyncExternalStore)(
|
|
331
|
+
registrySubscribe,
|
|
332
|
+
getRegistrySnapshot,
|
|
333
|
+
() => null
|
|
334
|
+
);
|
|
200
335
|
const instance = pipId ? registryInstance : context?.instance;
|
|
201
336
|
const subscribe = instance?.subscribe || emptySubscribe;
|
|
202
337
|
const getState = instance?.getState || emptyGetState;
|
|
203
|
-
const state = (0,
|
|
338
|
+
const state = (0, import_react6.useSyncExternalStore)(
|
|
204
339
|
subscribe,
|
|
205
340
|
getState,
|
|
206
341
|
emptyGetState
|
|
207
342
|
);
|
|
208
|
-
const [mounted, setMounted] = (0,
|
|
209
|
-
(0,
|
|
210
|
-
if (!instance && pipId) {
|
|
211
|
-
|
|
343
|
+
const [mounted, setMounted] = (0, import_react6.useState)(false);
|
|
344
|
+
(0, import_react6.useEffect)(() => setMounted(true), []);
|
|
345
|
+
if (!instance && pipId && mounted) {
|
|
346
|
+
const Comp2 = asChild ? Slot : "button";
|
|
347
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
348
|
+
Comp2,
|
|
349
|
+
{
|
|
350
|
+
ref,
|
|
351
|
+
disabled: true,
|
|
352
|
+
style: { opacity: 0.5, cursor: "not-allowed" },
|
|
353
|
+
...asChild ? {} : { type: "button" },
|
|
354
|
+
...rest,
|
|
355
|
+
children: asChild ? children : children ?? renderOpen ?? openLabel
|
|
356
|
+
}
|
|
357
|
+
);
|
|
212
358
|
}
|
|
213
359
|
if (!mounted) {
|
|
214
360
|
return null;
|
|
215
361
|
}
|
|
216
362
|
if (!isSupported2) {
|
|
217
363
|
if (renderUnsupported === null) return null;
|
|
218
|
-
return /* @__PURE__ */ (0,
|
|
364
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children: renderUnsupported });
|
|
219
365
|
}
|
|
220
|
-
const handleClick =
|
|
366
|
+
const handleClick = (e) => {
|
|
221
367
|
if (onClick) onClick(e);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
368
|
+
const target = instance ?? context?.instance;
|
|
369
|
+
target?.toggle();
|
|
225
370
|
};
|
|
226
371
|
const isOpen = state.isOpen;
|
|
372
|
+
if (hideInPip && isOpen && !!context) {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
227
375
|
const content = isOpen ? renderClose ?? closeLabel : renderOpen ?? openLabel;
|
|
228
376
|
const Comp = asChild ? Slot : "button";
|
|
229
|
-
return /* @__PURE__ */ (0,
|
|
377
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
230
378
|
Comp,
|
|
231
379
|
{
|
|
232
380
|
ref,
|
|
233
381
|
onClick: handleClick,
|
|
234
382
|
...asChild ? {} : { type: "button" },
|
|
235
383
|
...rest,
|
|
236
|
-
children: asChild ? children : content
|
|
384
|
+
children: asChild ? children : children ?? content
|
|
237
385
|
}
|
|
238
386
|
);
|
|
239
387
|
});
|
|
240
388
|
PipTrigger.displayName = "PipTrigger";
|
|
241
389
|
|
|
242
390
|
// src/usePip.ts
|
|
243
|
-
var
|
|
391
|
+
var import_react7 = require("react");
|
|
244
392
|
var import_core4 = require("@pip-it-up/core");
|
|
245
393
|
var emptyServerState3 = { isOpen: false, isSupported: false, pipWindow: null };
|
|
246
394
|
var emptyGetState2 = () => emptyServerState3;
|
|
247
395
|
function usePip(options = {}) {
|
|
248
|
-
const contentRef = (0,
|
|
249
|
-
const originRef = (0,
|
|
250
|
-
const instanceRef = (0,
|
|
396
|
+
const contentRef = (0, import_react7.useRef)(null);
|
|
397
|
+
const originRef = (0, import_react7.useRef)(null);
|
|
398
|
+
const instanceRef = (0, import_react7.useRef)(null);
|
|
251
399
|
if (!instanceRef.current) {
|
|
252
|
-
|
|
400
|
+
const { id: _id, ...factoryOptions } = options;
|
|
401
|
+
instanceRef.current = (0, import_core4.createPip)(factoryOptions);
|
|
253
402
|
}
|
|
254
403
|
const instance = instanceRef.current;
|
|
255
|
-
(0,
|
|
404
|
+
(0, import_react7.useEffect)(() => {
|
|
405
|
+
if (options.id) {
|
|
406
|
+
(0, import_core4.registerPip)(options.id, instance);
|
|
407
|
+
return () => (0, import_core4.unregisterPip)(options.id);
|
|
408
|
+
}
|
|
409
|
+
}, [options.id, instance]);
|
|
410
|
+
(0, import_react7.useEffect)(() => {
|
|
256
411
|
return () => {
|
|
257
412
|
instance.destroy();
|
|
413
|
+
instanceRef.current = null;
|
|
258
414
|
};
|
|
259
415
|
}, [instance]);
|
|
260
|
-
|
|
416
|
+
(0, import_react7.useLayoutEffect)(() => {
|
|
417
|
+
if (typeof instance.setDefaultElements === "function") {
|
|
418
|
+
instance.setDefaultElements({
|
|
419
|
+
contentEl: contentRef.current || void 0,
|
|
420
|
+
originEl: originRef.current || void 0
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}, [instance]);
|
|
424
|
+
const state = (0, import_react7.useSyncExternalStore)(
|
|
261
425
|
instance.subscribe,
|
|
262
426
|
instance.getState,
|
|
263
427
|
emptyGetState2
|
|
@@ -273,16 +437,6 @@ function usePip(options = {}) {
|
|
|
273
437
|
pipWindow: state.pipWindow
|
|
274
438
|
};
|
|
275
439
|
}
|
|
276
|
-
|
|
277
|
-
// src/usePipContext.ts
|
|
278
|
-
var import_react7 = require("react");
|
|
279
|
-
function usePipContext() {
|
|
280
|
-
const context = (0, import_react7.useContext)(PipContext);
|
|
281
|
-
if (!context) {
|
|
282
|
-
throw new Error("usePipContext must be used within a <PipWrapper>");
|
|
283
|
-
}
|
|
284
|
-
return context;
|
|
285
|
-
}
|
|
286
440
|
// Annotate the CommonJS export names for ESM import in node:
|
|
287
441
|
0 && (module.exports = {
|
|
288
442
|
PipContext,
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/PipWrapper.tsx
|
|
2
|
-
import { forwardRef, useEffect, useRef, useSyncExternalStore, useImperativeHandle } from "react";
|
|
3
|
-
import { createPip } from "@pip-it-up/core";
|
|
2
|
+
import { forwardRef, useEffect, useRef, useSyncExternalStore, useImperativeHandle, useLayoutEffect } from "react";
|
|
3
|
+
import { createPip, registerPip, unregisterPip } from "@pip-it-up/core";
|
|
4
4
|
|
|
5
5
|
// src/PipContext.tsx
|
|
6
6
|
import { createContext } from "react";
|
|
@@ -8,12 +8,29 @@ var PipContext = createContext(null);
|
|
|
8
8
|
|
|
9
9
|
// src/PipPortal.tsx
|
|
10
10
|
import { createPortal } from "react-dom";
|
|
11
|
+
|
|
12
|
+
// src/usePipContext.ts
|
|
13
|
+
import { useContext } from "react";
|
|
14
|
+
function usePipContext() {
|
|
15
|
+
const context = useContext(PipContext);
|
|
16
|
+
if (!context) {
|
|
17
|
+
throw new Error("usePipContext must be used within a <PipWrapper>");
|
|
18
|
+
}
|
|
19
|
+
return context;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/PipPortal.tsx
|
|
23
|
+
import { jsx } from "react/jsx-runtime";
|
|
11
24
|
var PipPortal = ({ children, pipWindow }) => {
|
|
12
|
-
|
|
25
|
+
const context = usePipContext();
|
|
26
|
+
return createPortal(
|
|
27
|
+
/* @__PURE__ */ jsx(PipContext.Provider, { value: { ...context, isInsidePip: true }, children }),
|
|
28
|
+
pipWindow.document.body
|
|
29
|
+
);
|
|
13
30
|
};
|
|
14
31
|
|
|
15
32
|
// src/PipWrapper.tsx
|
|
16
|
-
import { jsx } from "react/jsx-runtime";
|
|
33
|
+
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
17
34
|
var emptyServerState = { isOpen: false, isSupported: false, pipWindow: null };
|
|
18
35
|
var getServerState = () => emptyServerState;
|
|
19
36
|
var PipWrapper = forwardRef((props, ref) => {
|
|
@@ -24,6 +41,8 @@ var PipWrapper = forwardRef((props, ref) => {
|
|
|
24
41
|
as: Component = "div",
|
|
25
42
|
originAs: OriginComponent = "div",
|
|
26
43
|
children,
|
|
44
|
+
placeholder,
|
|
45
|
+
placeholderClassName,
|
|
27
46
|
...coreOptions
|
|
28
47
|
} = props;
|
|
29
48
|
const contentRef = useRef(null);
|
|
@@ -31,25 +50,45 @@ var PipWrapper = forwardRef((props, ref) => {
|
|
|
31
50
|
const instanceRef = useRef(null);
|
|
32
51
|
useImperativeHandle(ref, () => originRef.current);
|
|
33
52
|
if (!instanceRef.current) {
|
|
34
|
-
|
|
53
|
+
const { id: _id, mode: _mode, ...factoryOptions } = coreOptions;
|
|
54
|
+
if (_mode === "clone") {
|
|
55
|
+
console.warn(
|
|
56
|
+
'[PipWrapper] mode="clone" is not supported in the React package. PipWrapper always uses portal mode internally. For clone mode, use the vanilla createPip() API directly. Falling back to portal mode.'
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
instanceRef.current = createPip({ ...factoryOptions, mode: "portal" });
|
|
35
60
|
}
|
|
36
61
|
const instance = instanceRef.current;
|
|
37
62
|
useEffect(() => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
63
|
+
if (coreOptions.id) {
|
|
64
|
+
registerPip(coreOptions.id, instance);
|
|
65
|
+
return () => {
|
|
66
|
+
unregisterPip(coreOptions.id);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}, [coreOptions.id, instance]);
|
|
43
70
|
useEffect(() => {
|
|
44
71
|
return () => {
|
|
45
|
-
|
|
72
|
+
if (instanceRef.current) {
|
|
73
|
+
instanceRef.current.destroy();
|
|
74
|
+
}
|
|
46
75
|
};
|
|
47
|
-
}, [
|
|
76
|
+
}, []);
|
|
48
77
|
const state = useSyncExternalStore(
|
|
49
78
|
instance.subscribe,
|
|
50
79
|
instance.getState,
|
|
51
80
|
getServerState
|
|
52
81
|
);
|
|
82
|
+
useLayoutEffect(() => {
|
|
83
|
+
if (typeof instance.setDefaultElements === "function") {
|
|
84
|
+
instance.setDefaultElements({
|
|
85
|
+
contentEl: contentRef.current || void 0,
|
|
86
|
+
originEl: originRef.current || void 0
|
|
87
|
+
});
|
|
88
|
+
} else {
|
|
89
|
+
console.error("[PipWrapper] setDefaultElements is MISSING on instance!");
|
|
90
|
+
}
|
|
91
|
+
}, [instance, state.isOpen]);
|
|
53
92
|
const isControlled = controlledOpen !== void 0;
|
|
54
93
|
const prevOpenRef = useRef(state.isOpen);
|
|
55
94
|
useEffect(() => {
|
|
@@ -60,29 +99,121 @@ var PipWrapper = forwardRef((props, ref) => {
|
|
|
60
99
|
prevOpenRef.current = state.isOpen;
|
|
61
100
|
}
|
|
62
101
|
}, [state.isOpen, onOpenChange]);
|
|
102
|
+
const prevControlledOpenRef = useRef(false);
|
|
63
103
|
useEffect(() => {
|
|
64
104
|
if (isControlled) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
105
|
+
const changedToOpen = controlledOpen && !prevControlledOpenRef.current;
|
|
106
|
+
const changedToClosed = !controlledOpen && prevControlledOpenRef.current;
|
|
107
|
+
if (changedToOpen && !state.isOpen) {
|
|
108
|
+
if (contentRef.current) {
|
|
109
|
+
const rect = contentRef.current.getBoundingClientRect();
|
|
110
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
111
|
+
lastSizeRef.current = { width: rect.width, height: rect.height };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
instance.open().catch((err) => {
|
|
115
|
+
if (err.name === "NotAllowedError") {
|
|
116
|
+
console.warn('[PipWrapper] PiP window opening blocked: requires user activation. Ensure the "open" prop is changed within a user gesture handler.');
|
|
117
|
+
} else {
|
|
118
|
+
console.error("[PipWrapper] Failed to open PiP window", err);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
} else if (changedToClosed && state.isOpen) {
|
|
68
122
|
instance.close();
|
|
69
123
|
}
|
|
124
|
+
prevControlledOpenRef.current = controlledOpen;
|
|
70
125
|
}
|
|
71
126
|
}, [controlledOpen, isControlled, state.isOpen, instance]);
|
|
72
127
|
const defaultOpenHandled = useRef(false);
|
|
73
128
|
useEffect(() => {
|
|
74
129
|
if (!isControlled && defaultOpen && !defaultOpenHandled.current) {
|
|
75
130
|
defaultOpenHandled.current = true;
|
|
76
|
-
instance.open(
|
|
131
|
+
instance.open().catch((err) => {
|
|
132
|
+
if (err.name === "NotAllowedError") {
|
|
133
|
+
console.warn("[PipWrapper] PiP window defaultOpen blocked: requires user activation.");
|
|
134
|
+
} else {
|
|
135
|
+
console.error("[PipWrapper] Failed to open PiP window via defaultOpen", err);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
77
138
|
}
|
|
78
139
|
}, [defaultOpen, isControlled, instance]);
|
|
79
|
-
const mode = coreOptions.mode || "move";
|
|
80
|
-
|
|
140
|
+
const mode = coreOptions.mode === "clone" ? "move" : coreOptions.mode || "move";
|
|
141
|
+
const defaultPlaceholder = /* @__PURE__ */ jsxs("div", { style: { padding: "12px", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", backgroundColor: "transparent", border: "1px dashed color-mix(in srgb, currentColor 30%, #ccc)", borderRadius: "inherit", width: "100%", height: "100%", boxSizing: "border-box", overflow: "hidden" }, children: [
|
|
142
|
+
/* @__PURE__ */ jsx2("div", { style: { marginBottom: "4px", fontSize: "0.875rem", fontWeight: 500, opacity: 0.6, textAlign: "center" }, children: "\u{1F4FA} In PiP" }),
|
|
143
|
+
/* @__PURE__ */ jsx2("button", { onClick: () => instance.close(), style: { fontSize: "0.75rem", padding: "4px 8px", cursor: "pointer", borderRadius: "4px", border: "1px solid currentColor", background: "transparent", opacity: 0.6 }, children: "Restore" })
|
|
144
|
+
] });
|
|
145
|
+
const placeholderContent = placeholder !== void 0 ? placeholder : defaultPlaceholder;
|
|
146
|
+
const lastSizeRef = useRef({ width: 0, height: 0 });
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
if (!contentRef.current || state.isOpen) return;
|
|
149
|
+
const observer = new ResizeObserver((entries) => {
|
|
150
|
+
for (const entry of entries) {
|
|
151
|
+
if (entry.contentRect.width > 0 && entry.contentRect.height > 0) {
|
|
152
|
+
lastSizeRef.current = {
|
|
153
|
+
width: entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width,
|
|
154
|
+
height: entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
observer.observe(contentRef.current);
|
|
160
|
+
return () => observer.disconnect();
|
|
161
|
+
}, [state.isOpen]);
|
|
162
|
+
const prevIsOpenRef = useRef(state.isOpen);
|
|
163
|
+
useLayoutEffect(() => {
|
|
164
|
+
if (prevIsOpenRef.current && !state.isOpen && contentRef.current) {
|
|
165
|
+
const rect = contentRef.current.getBoundingClientRect();
|
|
166
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
167
|
+
lastSizeRef.current = { width: rect.width, height: rect.height };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
prevIsOpenRef.current = state.isOpen;
|
|
171
|
+
}, [state.isOpen]);
|
|
172
|
+
useLayoutEffect(() => {
|
|
173
|
+
if ((mode === "move" || mode === "portal") && state.isOpen && coreOptions.reserveSpace !== false && originRef.current) {
|
|
174
|
+
const { width, height } = lastSizeRef.current;
|
|
175
|
+
const origin = originRef.current;
|
|
176
|
+
if (width > 0 && height > 0) {
|
|
177
|
+
origin.style.minWidth = `${width}px`;
|
|
178
|
+
origin.style.minHeight = `${height}px`;
|
|
179
|
+
origin.style.width = `${width}px`;
|
|
180
|
+
origin.style.height = `${height}px`;
|
|
181
|
+
origin.style.display = "inline-block";
|
|
182
|
+
origin.style.verticalAlign = "top";
|
|
183
|
+
return () => {
|
|
184
|
+
origin.style.minWidth = "";
|
|
185
|
+
origin.style.minHeight = "";
|
|
186
|
+
origin.style.width = "";
|
|
187
|
+
origin.style.height = "";
|
|
188
|
+
origin.style.display = "";
|
|
189
|
+
origin.style.verticalAlign = "";
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}, [mode, state.isOpen, coreOptions.reserveSpace, instance]);
|
|
194
|
+
return /* @__PURE__ */ jsx2(PipContext.Provider, { value: { instance, state, isInsidePip: false }, children: /* @__PURE__ */ jsx2(OriginComponent, { ref: originRef, style: { display: "contents" }, children: (mode === "portal" || mode === "move") && state.isOpen && state.pipWindow ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
195
|
+
/* @__PURE__ */ jsx2(
|
|
196
|
+
"div",
|
|
197
|
+
{
|
|
198
|
+
className: placeholderClassName,
|
|
199
|
+
style: {
|
|
200
|
+
width: coreOptions.width ? `${coreOptions.width}px` : "100%",
|
|
201
|
+
height: lastSizeRef.current.height ? `${lastSizeRef.current.height}px` : "auto",
|
|
202
|
+
display: "inline-block",
|
|
203
|
+
verticalAlign: "top",
|
|
204
|
+
boxSizing: "border-box"
|
|
205
|
+
},
|
|
206
|
+
children: placeholderContent
|
|
207
|
+
},
|
|
208
|
+
"placeholder"
|
|
209
|
+
),
|
|
210
|
+
/* @__PURE__ */ jsx2(PipPortal, { pipWindow: state.pipWindow, children: /* @__PURE__ */ jsx2(Component, { ref: contentRef, children }) })
|
|
211
|
+
] }) : /* @__PURE__ */ jsx2(Component, { ref: contentRef, children }, "content") }) });
|
|
81
212
|
});
|
|
82
213
|
PipWrapper.displayName = "PipWrapper";
|
|
83
214
|
|
|
84
215
|
// src/PipTrigger.tsx
|
|
85
|
-
import { forwardRef as forwardRef3, useEffect as useEffect3, useState as useState2, useSyncExternalStore as useSyncExternalStore2, useContext } from "react";
|
|
216
|
+
import React3, { forwardRef as forwardRef3, useEffect as useEffect3, useState as useState2, useSyncExternalStore as useSyncExternalStore2, useContext as useContext2 } from "react";
|
|
86
217
|
import { getPip, subscribeRegistry } from "@pip-it-up/core";
|
|
87
218
|
|
|
88
219
|
// src/useIsPipSupported.ts
|
|
@@ -112,21 +243,21 @@ var Slot = forwardRef2((props, ref) => {
|
|
|
112
243
|
ref: (node) => {
|
|
113
244
|
if (typeof ref === "function") ref(node);
|
|
114
245
|
else if (ref) ref.current = node;
|
|
115
|
-
const childRef = children.ref;
|
|
246
|
+
const childRef = children.props.ref || children.ref;
|
|
116
247
|
if (typeof childRef === "function") childRef(node);
|
|
117
248
|
else if (childRef) childRef.current = node;
|
|
118
249
|
}
|
|
119
250
|
});
|
|
120
251
|
}
|
|
121
252
|
if (React2.Children.count(children) > 1) {
|
|
122
|
-
|
|
253
|
+
throw new Error("PipTrigger asChild expects a single React element child");
|
|
123
254
|
}
|
|
124
255
|
return null;
|
|
125
256
|
});
|
|
126
257
|
Slot.displayName = "Slot";
|
|
127
258
|
|
|
128
259
|
// src/PipTrigger.tsx
|
|
129
|
-
import { Fragment, jsx as
|
|
260
|
+
import { Fragment as Fragment2, jsx as jsx3 } from "react/jsx-runtime";
|
|
130
261
|
var emptySubscribe = () => () => {
|
|
131
262
|
};
|
|
132
263
|
var emptyServerState2 = { isOpen: false, isSupported: false, pipWindow: null };
|
|
@@ -140,22 +271,26 @@ var PipTrigger = forwardRef3((props, ref) => {
|
|
|
140
271
|
renderOpen,
|
|
141
272
|
renderClose,
|
|
142
273
|
renderUnsupported = null,
|
|
274
|
+
hideInPip = true,
|
|
143
275
|
onClick,
|
|
144
276
|
children,
|
|
145
277
|
...rest
|
|
146
278
|
} = props;
|
|
147
279
|
const isSupported2 = useIsPipSupported();
|
|
148
|
-
const context =
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const unsub = subscribeRegistry(pipId, () => {
|
|
154
|
-
setRegistryInstance(getPip(pipId));
|
|
155
|
-
});
|
|
156
|
-
return unsub;
|
|
157
|
-
}
|
|
280
|
+
const context = useContext2(PipContext);
|
|
281
|
+
const registrySubscribe = React3.useCallback((callback) => {
|
|
282
|
+
if (!pipId) return () => {
|
|
283
|
+
};
|
|
284
|
+
return subscribeRegistry(pipId, callback);
|
|
158
285
|
}, [pipId]);
|
|
286
|
+
const getRegistrySnapshot = React3.useCallback(() => {
|
|
287
|
+
return pipId ? getPip(pipId) : null;
|
|
288
|
+
}, [pipId]);
|
|
289
|
+
const registryInstance = useSyncExternalStore2(
|
|
290
|
+
registrySubscribe,
|
|
291
|
+
getRegistrySnapshot,
|
|
292
|
+
() => null
|
|
293
|
+
);
|
|
159
294
|
const instance = pipId ? registryInstance : context?.instance;
|
|
160
295
|
const subscribe = instance?.subscribe || emptySubscribe;
|
|
161
296
|
const getState = instance?.getState || emptyGetState;
|
|
@@ -166,41 +301,54 @@ var PipTrigger = forwardRef3((props, ref) => {
|
|
|
166
301
|
);
|
|
167
302
|
const [mounted, setMounted] = useState2(false);
|
|
168
303
|
useEffect3(() => setMounted(true), []);
|
|
169
|
-
if (!instance && pipId) {
|
|
170
|
-
|
|
304
|
+
if (!instance && pipId && mounted) {
|
|
305
|
+
const Comp2 = asChild ? Slot : "button";
|
|
306
|
+
return /* @__PURE__ */ jsx3(
|
|
307
|
+
Comp2,
|
|
308
|
+
{
|
|
309
|
+
ref,
|
|
310
|
+
disabled: true,
|
|
311
|
+
style: { opacity: 0.5, cursor: "not-allowed" },
|
|
312
|
+
...asChild ? {} : { type: "button" },
|
|
313
|
+
...rest,
|
|
314
|
+
children: asChild ? children : children ?? renderOpen ?? openLabel
|
|
315
|
+
}
|
|
316
|
+
);
|
|
171
317
|
}
|
|
172
318
|
if (!mounted) {
|
|
173
319
|
return null;
|
|
174
320
|
}
|
|
175
321
|
if (!isSupported2) {
|
|
176
322
|
if (renderUnsupported === null) return null;
|
|
177
|
-
return /* @__PURE__ */
|
|
323
|
+
return /* @__PURE__ */ jsx3(Fragment2, { children: renderUnsupported });
|
|
178
324
|
}
|
|
179
|
-
const handleClick =
|
|
325
|
+
const handleClick = (e) => {
|
|
180
326
|
if (onClick) onClick(e);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
327
|
+
const target = instance ?? context?.instance;
|
|
328
|
+
target?.toggle();
|
|
184
329
|
};
|
|
185
330
|
const isOpen = state.isOpen;
|
|
331
|
+
if (hideInPip && isOpen && !!context) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
186
334
|
const content = isOpen ? renderClose ?? closeLabel : renderOpen ?? openLabel;
|
|
187
335
|
const Comp = asChild ? Slot : "button";
|
|
188
|
-
return /* @__PURE__ */
|
|
336
|
+
return /* @__PURE__ */ jsx3(
|
|
189
337
|
Comp,
|
|
190
338
|
{
|
|
191
339
|
ref,
|
|
192
340
|
onClick: handleClick,
|
|
193
341
|
...asChild ? {} : { type: "button" },
|
|
194
342
|
...rest,
|
|
195
|
-
children: asChild ? children : content
|
|
343
|
+
children: asChild ? children : children ?? content
|
|
196
344
|
}
|
|
197
345
|
);
|
|
198
346
|
});
|
|
199
347
|
PipTrigger.displayName = "PipTrigger";
|
|
200
348
|
|
|
201
349
|
// src/usePip.ts
|
|
202
|
-
import { useRef as useRef2, useEffect as useEffect4, useSyncExternalStore as useSyncExternalStore3 } from "react";
|
|
203
|
-
import { createPip as createPip2 } from "@pip-it-up/core";
|
|
350
|
+
import { useRef as useRef2, useEffect as useEffect4, useSyncExternalStore as useSyncExternalStore3, useLayoutEffect as useLayoutEffect2 } from "react";
|
|
351
|
+
import { createPip as createPip2, registerPip as registerPip2, unregisterPip as unregisterPip2 } from "@pip-it-up/core";
|
|
204
352
|
var emptyServerState3 = { isOpen: false, isSupported: false, pipWindow: null };
|
|
205
353
|
var emptyGetState2 = () => emptyServerState3;
|
|
206
354
|
function usePip(options = {}) {
|
|
@@ -208,14 +356,30 @@ function usePip(options = {}) {
|
|
|
208
356
|
const originRef = useRef2(null);
|
|
209
357
|
const instanceRef = useRef2(null);
|
|
210
358
|
if (!instanceRef.current) {
|
|
211
|
-
|
|
359
|
+
const { id: _id, ...factoryOptions } = options;
|
|
360
|
+
instanceRef.current = createPip2(factoryOptions);
|
|
212
361
|
}
|
|
213
362
|
const instance = instanceRef.current;
|
|
363
|
+
useEffect4(() => {
|
|
364
|
+
if (options.id) {
|
|
365
|
+
registerPip2(options.id, instance);
|
|
366
|
+
return () => unregisterPip2(options.id);
|
|
367
|
+
}
|
|
368
|
+
}, [options.id, instance]);
|
|
214
369
|
useEffect4(() => {
|
|
215
370
|
return () => {
|
|
216
371
|
instance.destroy();
|
|
372
|
+
instanceRef.current = null;
|
|
217
373
|
};
|
|
218
374
|
}, [instance]);
|
|
375
|
+
useLayoutEffect2(() => {
|
|
376
|
+
if (typeof instance.setDefaultElements === "function") {
|
|
377
|
+
instance.setDefaultElements({
|
|
378
|
+
contentEl: contentRef.current || void 0,
|
|
379
|
+
originEl: originRef.current || void 0
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}, [instance]);
|
|
219
383
|
const state = useSyncExternalStore3(
|
|
220
384
|
instance.subscribe,
|
|
221
385
|
instance.getState,
|
|
@@ -232,16 +396,6 @@ function usePip(options = {}) {
|
|
|
232
396
|
pipWindow: state.pipWindow
|
|
233
397
|
};
|
|
234
398
|
}
|
|
235
|
-
|
|
236
|
-
// src/usePipContext.ts
|
|
237
|
-
import { useContext as useContext2 } from "react";
|
|
238
|
-
function usePipContext() {
|
|
239
|
-
const context = useContext2(PipContext);
|
|
240
|
-
if (!context) {
|
|
241
|
-
throw new Error("usePipContext must be used within a <PipWrapper>");
|
|
242
|
-
}
|
|
243
|
-
return context;
|
|
244
|
-
}
|
|
245
399
|
export {
|
|
246
400
|
PipContext,
|
|
247
401
|
PipTrigger,
|
package/package.json
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pip-it-up/react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "React components and hooks for the Document Picture-in-Picture API — PipWrapper, PipTrigger, usePip, useIsPipSupported",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"picture-in-picture",
|
|
7
|
+
"pip",
|
|
8
|
+
"react-pip",
|
|
9
|
+
"document-picture-in-picture",
|
|
10
|
+
"react-picture-in-picture",
|
|
11
|
+
"floating-window",
|
|
12
|
+
"pip-window",
|
|
13
|
+
"react-hooks",
|
|
14
|
+
"react-components",
|
|
15
|
+
"document-pip"
|
|
16
|
+
],
|
|
4
17
|
"repository": {
|
|
5
18
|
"type": "git",
|
|
6
19
|
"url": "git+https://github.com/Shakya47/pip-it-up.git",
|
|
@@ -29,11 +42,11 @@
|
|
|
29
42
|
"dist"
|
|
30
43
|
],
|
|
31
44
|
"dependencies": {
|
|
32
|
-
"@pip-it-up/core": "0.1.
|
|
45
|
+
"@pip-it-up/core": "0.1.5"
|
|
33
46
|
},
|
|
34
47
|
"peerDependencies": {
|
|
35
|
-
"react": ">=
|
|
36
|
-
"react-dom": ">=
|
|
48
|
+
"react": ">=18",
|
|
49
|
+
"react-dom": ">=18"
|
|
37
50
|
},
|
|
38
51
|
"devDependencies": {
|
|
39
52
|
"@testing-library/jest-dom": "^6.9.1",
|