Socket
Socket
Sign inDemoInstall

react-useportal

Package Overview
Dependencies
Maintainers
1
Versions
49
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

react-useportal - npm Package Compare versions

Comparing version 0.1.46 to 1.0.0

39

dist/usePortal.d.ts

@@ -1,3 +0,13 @@

import { ReactNode } from 'react';
interface UsePortalOptions {
import { DOMAttributes, EventHandler, SyntheticEvent, MutableRefObject } from 'react';
declare type HTMLElRef = MutableRefObject<HTMLElement>;
declare type CustomEvent = {
event?: SyntheticEvent<any, Event>;
portal: HTMLElRef;
targetEl: HTMLElement;
};
declare type CustomEventHandler = (customEvent: CustomEvent) => void | HTMLElRef;
declare type EventHandlers = {
[K in keyof DOMAttributes<K>]?: EventHandler<SyntheticEvent<any, Event>>;
};
declare type UsePortalOptions = {
closeOnOutsideClick?: boolean;

@@ -10,22 +20,7 @@ closeOnEsc?: boolean;

stateful?: boolean;
}
export default function usePortal({ closeOnOutsideClick, closeOnEsc, renderOnClickedElement, renderBelowClickedElement, // appear directly under the clicked element/node in the DOM
bindTo, // attach the portal to this node in the DOM
isOpen: defaultIsOpen, stateful, }?: UsePortalOptions): (boolean | ((e: any) => number | void) | (({ children }: {
children: ReactNode;
}) => import("react").ReactPortal | null))[] & {
isOpen: boolean;
openPortal: (e: any) => number | undefined;
onMouseDown: (e: any) => void;
ref: import("react").MutableRefObject<undefined>;
closePortal: () => void;
togglePortal: (e: any) => number | void;
Portal: ({ children }: {
children: ReactNode;
}) => import("react").ReactPortal | null;
bind: {
onMouseDown: (e: any) => void;
ref: import("react").MutableRefObject<undefined>;
};
};
onOpen?: CustomEventHandler;
onClose?: CustomEventHandler;
} & EventHandlers;
export default function usePortal({ closeOnOutsideClick, closeOnEsc, renderOnClickedElement, renderBelowClickedElement, bindTo, // attach the portal to this node in the DOM
isOpen: defaultIsOpen, onOpen, onClose, ...eventHandlers }?: UsePortalOptions): any;
export {};
"use strict";
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
var __importDefault = (this && this.__importDefault) || function (mod) {

@@ -9,5 +20,6 @@ return (mod && mod.__esModule) ? mod : { "default": mod };

const use_ssr_1 = __importDefault(require("use-ssr"));
function usePortal({ closeOnOutsideClick = true, closeOnEsc = true, renderOnClickedElement, renderBelowClickedElement, // appear directly under the clicked element/node in the DOM
bindTo, // attach the portal to this node in the DOM
isOpen: defaultIsOpen = false, stateful = true, } = {}) {
const errorMessage1 = 'You must either bind to an element or pass an event to openPortal(e).';
function usePortal(_a = {}) {
var { closeOnOutsideClick = true, closeOnEsc = true, renderOnClickedElement, renderBelowClickedElement, bindTo, // attach the portal to this node in the DOM
isOpen: defaultIsOpen = false, onOpen, onClose } = _a, eventHandlers = __rest(_a, ["closeOnOutsideClick", "closeOnEsc", "renderOnClickedElement", "renderBelowClickedElement", "bindTo", "isOpen", "onOpen", "onClose"]);
const { isServer, isBrowser } = use_ssr_1.default();

@@ -17,24 +29,27 @@ const [isOpen, makeOpen] = react_1.useState(defaultIsOpen);

const open = react_1.useRef(isOpen);
const setOpen = react_1.useCallback(v => {
const setOpen = react_1.useCallback((v) => {
// workaround to not have stale `isOpen` in the handleOutsideMouseClick
open.current = v;
makeOpen(v);
}, [isOpen, open.current]);
const renderByRef = react_1.useRef();
const portal = react_1.useRef(isBrowser && document.createElement('div'));
}, []);
const targetEl = react_1.useRef(); // this is the element you are clicking/hovering/whatever, to trigger opening the portal
const portal = react_1.useRef(isBrowser ? document.createElement('div') : null);
react_1.useEffect(() => {
if (isBrowser && !portal.current)
portal.current = document.createElement('div');
}, [isBrowser]);
}, [isBrowser, portal]);
const elToMountTo = react_1.useMemo(() => {
if (isServer)
return;
return ((bindTo && react_dom_1.findDOMNode(bindTo)) || document.body);
return (bindTo && react_dom_1.findDOMNode(bindTo)) || document.body;
}, [isServer, bindTo]);
const handleKeydown = react_1.useCallback(e => {
var ESC = 27;
if (e.keyCode === ESC && stateful && closeOnEsc)
setOpen(false);
}, [closeOnEsc, stateful, setOpen]);
const openPortal = react_1.useCallback(e => {
const handleEvent = react_1.useCallback((func, event) => {
if (event && event.currentTarget)
targetEl.current = event.currentTarget;
// i.e. onClick, etc. inside usePortal({ onClick({ portal, targetEl }) {} })
const maybePortal = func({ portal, targetEl: targetEl.current, event });
if (maybePortal)
portal.current = maybePortal.current;
}, [portal, targetEl]);
const openPortal = react_1.useCallback((event) => {
if (isServer)

@@ -45,38 +60,61 @@ return;

// setTimeout, but for now this works
if (e == null)
return setTimeout(() => stateful && setOpen(true), 0);
if (e && e.nativeEvent)
e.nativeEvent.stopImmediatePropagation();
const { left, top, height } = e.target.getBoundingClientRect();
if (event == null && targetEl.current == null) {
setTimeout(() => setOpen(true), 0);
throw Error(errorMessage1);
}
if (event && event.nativeEvent)
event.nativeEvent.stopImmediatePropagation();
if (event)
targetEl.current = event.currentTarget;
if (!targetEl.current)
throw Error(errorMessage1);
const { left, top } = targetEl.current.getBoundingClientRect();
if (onOpen)
handleEvent(onOpen, event);
if (renderOnClickedElement && portal.current instanceof HTMLElement) {
portal.current.style.height = '0px';
portal.current.style.position = 'absolute';
portal.current.style.left = left + 'px';
portal.current.style.top = top + 'px';
portal.current.style.cssText = `
height: 0px;
position: absolute;
left: ${left}px;
top: ${top}px;
`;
}
else if (renderBelowClickedElement && portal.current instanceof HTMLElement) {
portal.current.style.position = 'absolute';
portal.current.style.left = left + 'px';
portal.current.style.top = top + height + 'px';
portal.current.style.cssText = `
position: absolute;
left: ${left};
top: ${top};
`;
}
stateful && setOpen(true);
}, [isServer, stateful, portal, setOpen, renderBelowClickedElement, renderOnClickedElement]);
const closePortal = react_1.useCallback(() => {
setOpen(true);
}, [isServer, portal, setOpen, renderBelowClickedElement, renderOnClickedElement, handleEvent, targetEl, onOpen]);
const closePortal = react_1.useCallback((event) => {
if (isServer)
return;
if (onClose)
handleEvent(onClose, event);
if (open.current)
setOpen(false);
}, [isServer, isOpen, open.current, setOpen]);
const togglePortal = react_1.useCallback(e => (isOpen ? setOpen(false) : openPortal(e)), [isOpen, open.current, setOpen, openPortal]);
const handleOutsideMouseClick = react_1.useCallback(({ target, button }) => {
if (isServer || !(portal.current instanceof HTMLElement))
}, [isServer, handleEvent, onClose, setOpen]);
const togglePortal = react_1.useCallback((e) => isOpen ? closePortal(e) : openPortal(e), [isOpen, closePortal, openPortal]);
const handleKeydown = react_1.useCallback(e => {
var ESC = 27;
if (e.keyCode === ESC && closeOnEsc)
closePortal(e);
}, [closeOnEsc, closePortal]);
const handleOutsideMouseClick = react_1.useCallback(e => {
if (isServer)
return;
if (portal.current.contains(target) || button !== 0 || !open.current)
if (!(portal.current instanceof HTMLElement))
return;
if (stateful && closeOnOutsideClick)
closePortal();
}, [isServer, isOpen, stateful, closePortal, closeOnOutsideClick]);
if (portal.current.contains(e.target) || e.button !== 0 || !open.current)
return;
if (closeOnOutsideClick)
closePortal(e);
}, [isServer, closePortal, closeOnOutsideClick, portal]);
react_1.useEffect(() => {
if (isServer || !(elToMountTo instanceof HTMLElement) || !(portal.current instanceof HTMLElement))
if (isServer)
return;
if (!(elToMountTo instanceof HTMLElement) || !(portal.current instanceof HTMLElement))
return;
const node = portal.current;

@@ -91,29 +129,20 @@ elToMountTo.appendChild(portal.current);

};
}, [isServer, handleOutsideMouseClick, handleKeydown, elToMountTo]);
}, [isServer, handleOutsideMouseClick, handleKeydown, elToMountTo, portal]);
const Portal = react_1.useCallback(({ children }) => {
if (portal.current instanceof HTMLElement)
if (portal.current != null)
return react_dom_1.createPortal(children, portal.current);
return null;
}, []);
return Object.assign([
openPortal,
closePortal,
open.current,
Portal,
togglePortal
], {
isOpen: open.current,
openPortal,
onMouseDown: handleKeydown,
ref: renderByRef,
closePortal,
}, [portal]);
// this should handle all eventHandlers like onClick, onMouseOver, etc. passed into the config
const customEventHandlers = Object
.entries(eventHandlers)
.reduce((acc, [handlerName, eventHandler]) => {
acc[handlerName] = (event) => handleEvent(eventHandler, event);
return acc;
}, {});
return Object.assign([openPortal, closePortal, open.current, Portal, togglePortal], Object.assign(Object.assign({ isOpen: open.current, openPortal, onMouseDown: handleKeydown, ref: targetEl, closePortal,
togglePortal,
Portal,
bind: {
onMouseDown: handleKeydown,
ref: renderByRef
}
});
Portal }, customEventHandlers), { bind: Object.assign({ onMouseDown: handleKeydown, ref: targetEl }, customEventHandlers) }));
}
exports.default = usePortal;
//# sourceMappingURL=usePortal.js.map
{
"name": "react-useportal",
"version": "0.1.46",
"version": "1.0.0",
"homepage": "https://codesandbox.io/s/w6jp7z4pkk",

@@ -16,6 +16,15 @@ "main": "dist/usePortal.js",

"dev": "rm -rf dist && parcel examples/index.html --open",
"prepublishOnly": "yarn build # runs before publish",
"build": "rm -rf dist && ./node_modules/.bin/tsc --module CommonJS",
"prepublishOnly": "yarn build",
"test": "tsc -p . --noEmit && tsc -p . && jest",
"test:watch": "tsc -p . --noEmit && tsc -p . && jest --watch"
"build:watch": "rm -rf dist && ./node_modules/.bin/tsc -w --module CommonJS",
"test:browser": "yarn tsc && jest --env=jsdom",
"test:browser:watch": "yarn tsc && jest --watch --env=jsdom",
"test:server": "yarn tsc && jest --env=node",
"test:server:watch": "yarn tsc && jest --watch --env=node",
"test:watch": "yarn test:browser:watch && yarn test:server:watch",
"test": "yarn test:browser && yarn test:server",
"clean": "npm prune; yarn cache clean; rm -rf ./node_modules package-lock.json yarn.lock; yarn",
"lint": "eslint ./**/*.{ts,tsx}",
"lint:fix": "npm run lint -- --fix",
"lint:watch": "watch 'yarn lint'"
},

@@ -30,10 +39,17 @@ "peerDependencies": {

"devDependencies": {
"@types/jest": "^24.0.12",
"@types/react": "^16.8.8",
"@types/jest": "^24.0.18",
"@types/react": "^16.9.2",
"@types/react-dom": "^16.8.4",
"@typescript-eslint/eslint-plugin": "^2.2.0",
"@typescript-eslint/parser": "^2.2.0",
"eslint": "^6.3.0",
"eslint-plugin-jest": "^22.17.0",
"eslint-plugin-prettier": "^3.1.0",
"eslint-plugin-react": "^7.14.3",
"jest": "^24.7.1",
"parcel-bundler": "^1.12.3",
"prettier": "^1.18.2",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-hooks-testing-library": "^0.5.0",
"react-hooks-testing-library": "^0.6.0",
"react-test-renderer": "^16.8.6",

@@ -44,33 +60,2 @@ "react-testing-library": "^8.0.0",

},
"jest": {
"transform": {
".(ts|tsx)": "ts-jest"
},
"testEnvironment": "node",
"testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx|js)$",
"testPathIgnorePatterns": [
"/node_modules/",
"/dist/"
],
"moduleFileExtensions": [
"ts",
"tsx",
"js"
],
"coveragePathIgnorePatterns": [
"/node_modules/",
"/dist/"
],
"coverageThreshold": {
"global": {
"branches": 90,
"functions": 95,
"lines": 95,
"statements": 95
}
},
"collectCoverageFrom": [
"src/*.{js,ts}"
]
},
"files": [

@@ -77,0 +62,0 @@ "dist"

@@ -13,3 +13,3 @@ <p style="text-align: center;" align="center">

<a href="https://www.npmjs.com/package/react-useportal">
<img src="https://img.shields.io/npm/dm/react-useportal.svg" />
<img src="https://img.shields.io/npm/dt/react-useportal.svg" />
</a>

@@ -58,2 +58,3 @@ <a href="https://bundlephobia.com/result?p=react-useportal">

- [Modal Example - create-react-app](https://codesandbox.io/s/w6jp7z4pkk)
- [Dropdown Example (useDropdown) - Next.js](https://codesandbox.io/s/useportal-usedropdown-587fo)

@@ -75,9 +76,21 @@

<Portal>
This text is portaled at the end of document.body!
</Portal>
const App = () => {
const { Portal } = usePortal()
<Portal bindTo={document && document.getElementById('san-francisco')}>
This text is portaled into San Francisco!
</Portal>
return (
<Portal>
This text is portaled at the end of document.body!
</Portal>
)
}
const App = () => {
const { Portal } = usePortal()
return (
<Portal bindTo={document && document.getElementById('san-francisco')}>
This text is portaled into San Francisco!
</Portal>
)
}
```

@@ -136,2 +149,22 @@

```
### Customizing the Portal directly
By using `onOpen`, `onClose` or any other event handler, you can modify the `portal` and return it. See [useDropdown](https://codesandbox.io/s/useportal-usedropdown-587fo) for a working example. It's important that you pass the `event` object to `openPortal`.
```jsx
const App = () => {
const { openPortal, isOpen } = usePortal({
onOpen({ portal }) {
portal.current.style.cssText = `
position: absolute;
/* add your custom styles here! */
`
return portal
}
})
return <button onClick={e => openPortal(e)}>Click Me<button>
}
```
**Make sure you are passing the html synthetic event to the `openPortal`. i.e. `onClick={e => openPortal(e)}`**

@@ -148,2 +181,5 @@

| `isOpen` | This will be the default for the portal. Default is `false` |
| `onOpen` | This is used to call something when the portal is opened and to modify the css of the portal directly |
| `onClose` | This is used to call something when the portal is closed and to modify the css of the portal directly |
| html event handlers (i.e. `onClick`) | These can be used instead of `onOpen` to modify the css of the portal directly |

@@ -164,2 +200,6 @@ ### Option Usage

isOpen: false,
onOpen: ({ event, portal, targetEl }) => {},
onClose({ event, portal, targetEl }) {},
// in addition, any event handler such as onClick, onMouseOver, etc will be handled like
onClick({ event, portal, targetEl }) {}
})

@@ -177,2 +217,4 @@ ```

- [ ] tests (priority)
- [ ] maybe have a `<Provider order={['Portal', 'openPortal']} />` then you can change the order of the array destructuring syntax
- [ ] instead of having a `stateful` option, just make `usePortal` stateful, and allow `import { Portal } from 'react-useportal'`
- [ ] make work without requiring the html synthetic event

@@ -182,3 +224,5 @@ - [ ] add example for tooltip (like [this one](https://codepen.io/davidgilbertson/pen/ooXVyw))

- [ ] fix code so maintainability is A
- [ ] set up code climate test coverage
- [ ] optimize badges [see awesome badge list](https://github.com/boennemann/badges)
- [ ] add code climate test coverage badge
- [X] document when you are required to have synthetic event

@@ -185,0 +229,0 @@ - [X] make isomorphic

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc