I'm wondering if there is any built-in way to sync the reference width with the popper element? Looked through the docs but didn't find anything helpful. Tried with computeStyle but with no luck.
If it doesn't exist I think it would be a nice feature addition unless it's super simple to implement yourself.
Note that I'm using Popper in a react environment so applyStyle is off
PS: Sorry that I didn't use the issue template but it didn't really fit in this case
Not sure about modifiers but you can apply it in onUpdate & onCreate;
function setWidth({instance: {reference, popper}}) {
// box-sizing: border-box
popper.style.width = `${reference.offsetWidth}px`;
}
const options = {
onCreate: setWidth,
onUpdate: setWidth
};
Edit: you need to double-update for this to work though
The same can be done as a modifier, just put the same @atomiks logic inside it and run it before the computeStyles modifier
Oh can you provide an example of that? I don't think fully understand 馃
Like this @FezVrasta? The order number has always confused me
const options = {
modifiers: {
setPopperWidth: {
enabled: true,
order: 849,
fn: setWidth, // (the function defined before)
}
}
};
Whoa! I had no idea that you could add custom modifers!
It sets the correct width now but the placement stopped working https://codesandbox.io/s/cool-rosalind-ss23s?fontsize=14
Edit: Solved it; the modifier had to return data but the placement is still off
When in doubt you can always do a double update, it solves various problems like #784 (and I edited my earlier comment) 馃槅
Whoa! I had no idea that you could add custom modifers!
https://github.com/FezVrasta/popper.js#writing-modifiers-on-your-own
馃様
@atomiks What do you mean with a double update? There is no way to get around it without updating twice?
@FezVrasta Crap, I only read the website docs 馃槙
I think the problem is: modifiers are run after the size has been computed? No order number seems to work (0 or 1000) - but then it works when resizing (aka double update)
function update() {
// ...
// here if 'auto' placement
data.placement = computeAutoPlacement(...);
data.originalPlacement = data.placement;
data.positionFixed = this.options.positionFixed;
// here mainly though?
data.offsets.popper = getPopperOffsets(...);
data.offsets.popper.position = this.options.positionFixed ? 'fixed' : 'absolute';
data = runModifiers(this.modifiers, data);
// ...
}
I see, if that's the case it would be nice to have the modifier run before (maybe order of -1?)
I _think_ I managed to solve this without a double update, but it's not beautiful 馃槄 Since the width should be the same I figured it'd be "safe" to just use the reference left/right values.
function setWidth(data) {
const { width, left, right } = data.offsets.reference;
data.styles.width = width;
data.offsets.popper.width = width;
data.offsets.popper.left = left;
data.offsets.popper.right = right;
return data;
}
Closing as the solution above worked in all my use-cases.
Thanks so much for the fast replies btw 馃檶
I'm assuming it doesn't work for left/right placement?
I wonder if we need negative modifiers as you said, or just a onBeforeUpdate callback?
No that's true, but I don't think that's a common scenario. In my case I wanted a dropdown listbox/menu to have the same width as the button.
But if we run it before how would we know the reference width?
I mean, its position will be incorrect for those placements rather than the width.
But if we run it before how would we know the reference width?
Not sure what you mean. The onBeforeUpdate will be called at the top of update() before the reference & popper size are calculated (where you'll apply the mutations). So it should work fine.
Okay, but then you'd need to read the DOM yourself (getBoundingClientRect), because the offsets haven't been calculated by Popper yet? I just swiftly went through the source so I'm probably incorrect.
Another solution could be a util that calculates the new position with the existing offsets and placement values, I believe that should work too.
You're right though, it doesn't make sense for left/right anyway. If you wanted to add a safe-guard in case you are using left/right/auto placement somewhere else and have this modifier, you can just have an early return anyway to prevent the behavior.
if (!['top', 'bottom'].includes(data.placement.split('-')[0])) {
return data;
}
I think your solution is fine 馃憤
I see, if that's the case it would be nice to have the modifier run before (maybe order of -1?)
I _think_ I managed to solve this without a double update, but it's not beautiful 馃槄 Since the width should be the same I figured it'd be "safe" to just use the reference left/right values.
function setWidth(data) { const { width, left, right } = data.offsets.reference; data.styles.width = width; data.offsets.popper.width = width; data.offsets.popper.left = left; data.offsets.popper.right = right; return data; }
I can't get this to work with the current version of popper. I think the shape of data has changed I tried a setting width on a few different places on the data object but I couldn't get a working version and in face anything I return from the modifier function returns an error.
I've read the docs on custom modification but there is no example of modifying the popper state. I'm using React but I don't think that is the issue. here is an example of the error.
function setWidth({state}) {
console.log("data", data);
//remove this return to remove the error
return state;
}
you need to either return state or null, the function argument is an object, withstate as one of its properties.
Thank you. To update the popper width should I mutate a property on state, if so which one?
Take a look at this example
If you can ensure the placement to use the -start variation when the modifier is enabled, you can also get rid of the DOM mutation:
Popper.createPopper(button, tooltip, {
placement: "bottom-start",
modifiers: [
{
name: "sameWidth",
enabled: true,
fn: ({ state }) => {
state.styles.popper.width = `${state.rects.reference.width}px`;
},
phase: "beforeWrite",
requires: ["computeStyles"],
}
]
});
phase: "beforeWrite was the missing piece. Thanks.
Here is a working react version if anyone needs it.
FYI if anyone is trying to get this to work with usePopper, make sure you wrap modifiers in a useMemo call, or define it outside of your component. Otherwise, usePopper will recompute the instance on every render because the modifiers object will have changed. This tripped me up for a while so figured I'd share :)
const modifiers = useMemo(() => [
{
name: "sameWidth",
enabled: true,
fn: ({ state }) => {
state.styles.popper.width = `${state.rects.reference.width}px`;
},
phase: "beforeWrite",
requires: ["computeStyles"],
}
], []);
const {styles, attributes} = usePopper(referenceElement, popperElement, {
placement: 'bottom-start',
modifiers,
});
@traviskaufman you could also just move the object outside of the function so it stays static all the time
To prevent position problems don't forget to use 'effect':
const popperModifiers = useMemo(
() => [
{
name: 'sameWidth',
enabled: true,
phase: 'beforeWrite',
requires: [ 'computeStyles' ],
fn({ state }) {
state.styles.popper.width = `${state.rects.reference.width}px`;
},
effect({ state }) {
state.elements.popper.style.width = `${
state.elements.reference.offsetWidth
}px`;
}
},
],
[ ]
);
I followed @atomiks suggestion and used double updates (best thing that worked for me in all cases), also made it work with either height or width depending on the placement
{
name: "matchReferenceSize",
enabled: true,
fn: ({ state, instance }) => {
const widthOrHeight =
state.placement.startsWith("left") ||
state.placement.startsWith("right")
? "height"
: "width";
const popperSize = state.rects.popper[widthOrHeight];
const referenceSize = state.rects.reference[widthOrHeight];
if (popperSize >= referenceSize) return;
state.styles.popper[widthOrHeight] = `${referenceSize}px`;
instance.update();
},
phase: "beforeWrite",
requires: ["computeStyles"]
}
This seems to cause an infinite loop when using react-popper: https://popper.js.org/react-popper/
You need to define the custom modifier in a useMemo, or outside the render scope.
Most helpful comment
If you can ensure the
placementto use the-startvariation when the modifier is enabled, you can also get rid of the DOM mutation: