react-useportal
Advanced tools
Comparing version 0.1.46 to 1.0.0
@@ -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
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
25454
168
0
224
19
1