Demos here: https://runfaj.github.io/react-nested-popper/
react-nested-popper is a react library based on V2 popper.js, but with added features created to handle a number of popper scenarios that other libraries fail to capture:
- handling for nesting and popper groups, combined with outside clicks
- proper handling for context with nested popper content
- full implementation of popper.js allowing all popper options
- support for portals to decouple the popper content
This library is an unstyled, functionality-only library.
We used react-popper for a long time and have liked it. However, we kept finding that there were situations where we couldn't do things because of the need for stackd nested poppers and the inability to upgrade some of our packages. We've tried other similar libraries, but none met our needs. Once popper.js V2 came out, the decision was made to put in the effort to make our own popper library. So...here we are!
Install the Package:
npm install react-nested-popper
or yarn add react-nested-popper
Add to your jsx file:
import { Popper, Target, Content } from 'react-nested-popper';
// render
<Popper>
<Target>{/* The target that the popper is tied to here */}</Target>
<Content>{/* The content to appear in the popper here */}</Content>
</Popper>
Note 1: The Popper must always have 1 Target and 1 Content
Note 2: react and react-dom are required peer dependencies
The react-nested-popper is created firstly to handle multiple nested poppers. This is achieved by a "stack". By default, all popper instances will determine their own stack, but you can create your own stacks as needed.
In addition, you can create a controlled popper (the hide/show state is managed by you), or an uncontrolled popper (the hide/show state is managed by the library).
With that in mind, here's the options available for the three components:
Prop | Default value | Controlled or Uncontrolled | Description |
---|---|---|---|
initiallyOpen | false | uncontrolled | Set to true to have the popper initially open when mounted. |
outsideClickType | 'default' | uncontrolled | 'default' will close the top most open popper in a popper stack. 'group' will close all poppers in a given stack. 'all' will close all poppers in all stack groups. |
targetToggle | false | uncontrolled | Set to true if you want the target to toggle the popper. By default clicking the target will only open the popper. |
shouldCloseOnOutsideClick | (e) => false | both | If this returns true, enables closing the popper by clicking outside the popper area. |
groupName | 'auto' | both | See section below on the groupName. |
onOutsideClick | '(contentInstance, e)=>{}' | both | Method called any time a click occurs outside the popper content (target excluded). Convenience method only. Use onPopperWillClose to handle controlled closing. |
portalClassName | '' | both | Classname string to add to the portal element. |
portalRoot | '' | both | If you want your portal to appear somewhere other than the end of the body, specify the target dom element to put it in. |
usePortal | true | both | Set to false to not use a portal and just do the popper in the same element location. |
onPopperWillClose | '()=>{}' | controlled | If something is supposed to close the popper (e.g. Stack.destroyStack was called), this method will be called so you can update your show attribute accordingly. |
onTargetClick | '(e)=>{}' | controlled | Method to call when the target is clicked on. Useful if you don't want to wrap the target content in another element (e.g. text only). |
show | null | controlled | If not null or undefined, setting this to true/false makes the popper a controlled component. |
Prop | Default value | Description |
---|---|---|
className | '' | Classname string to add to the target element. |
innerRef | '(el) => {}' | If you need a ref to the target element for some reason, use this. |
Prop | Default value | Description |
---|---|---|
arrowClassName | '' | Classname string to add to the arrow element. |
className | '' | Classname string to add to the content element. |
includeArrow | false | Adds the arrow element (with data-popper-arrow attribute) if you want your popper to have an arrow. See popper.js for styling your arrow. |
innerRef | '(el) => {}' | If you need a ref to the content element for some reason, use this. |
onClick | '(e) => {}' | Regular onClick method for clicking in the content area if needed. |
popperOptions | {} | Standard object of popper options as outlined by popper.js. |
You can also manually use the Stack util, should you need. Here's the public methods:
Method | Description |
---|---|
getStack(stackName='global') | Gets the array of popper instances for a given stack name. Creates a new empty stack if stack name isn't previously defined. |
destroyStack(stackName=true) | Destroys all popper instances in a stack. Use `true` as the value to destroy all instances in all stacks. |
<Popper
(default case: groupName="auto")
(or)
groupName="string"
(or)
groupName={["string", "string"]}
>
The group name for popper is a bit complicated, so merits some explanation.
The group name on a popper specifies which group(s) a set of poppers belongs to. For example, if you wanted to have two poppers open at the same time, but then close in the order you opened them, you could specify each popper to belong to the same group. Alternately, you could have two poppers open at the same time and close at the same time with different groups.
This is also useful for nesting, as nesting poppers and putting them in the same group will only close the top most item in the group when clicking outside. The opposite with different groups might be dropdowns where only one should be open at a given time.
With all of these cases, please see the demos to visually see how the group name affects nesting. First we'll look at custom group names, then the default "auto" case.
You can define the groupName as any string you'd like (except "auto"). For example, here's a "global" group:
<Popper
groupName="global"
>
Depending on the need, you may need to have a set of poppers belong to a different group though. For example, maybe you want multiple popovers to open independent of each other.
<Popper
groupName="popper1"
>
<Popper
groupName="popper2"
>
If you define multiple poppers as the same group (regardless of physically nested or not), they will close in the opposite order they were opened.
<Popper
groupName="global"
>
<Popper
groupName="global"
>
Sometimes, a single group name for poppers isn't enough, like having a nested popper, but each nested item should toggle from other items. What does this mean? Let's look at a specific example.
Say I made a component that was a popper with a form inside. On this form were two dropdowns, each being their own popper component. In this situation, I might want to keep the parent popper open, but only allow one child dropdown to be open at a time.
We can't accomplish this with just one group because the dropdowns need to have separate groups to only allow one to be open. But, they both need to belong to the parent popper group. So, that's where multiple group names are needed.
<Popper
groupName="popper"
>
<Dropdown
groupName={["popper", "dropdown1"]}
>
<Dropdown
groupName={["popper", "dropdown2"]}
>
</Popper>
The array of group names is arranged from lowest to highest group, so in this case, the lowest open item would be the popper group, then the dropdown group.
By default, groupName will be "auto". This means that the nesting will try to determine stacks by itself. With auto, the functionality behaves as follows:
- If no other popper is open, create a new stack and add this popper to it.
- If another popper is open, look to see if the target for this popper belongs to the open popper:
- If doesn't belong to another popper, add to new stack.
- If belongs to another popper:
- find the parent popper in the stack.
- Move any poppers that are children of the parent to a new stack
- Add the new popper to the same stack as the parent.
To interpret these rules, we basically do somemthing similar to the example mentioned in the Multiple Groups above, but automatically determine the group names so you don't have to.
This is the main point of this package. Basically, when a popper belongs to a group, we create a stack of poppers that belong to the given groups defined for each popper. We use this stack to track the last opened item and only close the last opened item in the group.
With multiple groups, we treat the multiple groups as a mini-stack as well, where we only close the last item of the list of groups where a group contains an item. Confused yet? Anyway, this approach allows for binding multiple groups while only closing the top most item as needed each time.
This package is published under the MIT License.
Thanks to:
- Grow.com, for the amazing place to work and build software
- robskidmore, for helping plan out this package
- runfaj, for creating this package
- All contributors (code, issues, documentation, etc.) for helping make this package continually better
- What's with all the weird dependencies? Nearly all the dependencies listed are for the demo. The webpack bundle splits out to where the only things that are actually used in this package are a couple lodash methods and the popperjs library. react and react-dom are not included as they are peer dependencies.