Replies: 3 comments 12 replies
-
I like the idea but I don't think I understand what's the difference between |
Beta Was this translation helpful? Give feedback.
-
More architectural ideasModifier mutations are entirely order-based and pure for the consumerCurrently, modifiers directly mutate This can be made pure and less prone to bugs in the following way (inside let offsets = initialOffsetsFromPlacement;
modifiers.forEach((modifier) => {
offsets = modifier.getOffsets({
...options,
// As long as the order is correct, the modifier
// will start from the right place.
offsets,
rects: {
popper: popperRect,
reference: referenceRect,
},
});
}); Now, // super basic example
const preventOverflow = (features) => {
return {
name: 'preventOverflow',
getOffsets({placement, rects, offsets}) {
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
// already takes into account the `offset` modifier because of the ordering
let x = offsets.x;
let y = offsets.y;
x = Math.max(0, x);
y = Math.max(0, y);
if (x + rects.popper.width > windowWidth) {
x = windowWidth - rects.popper.width;
}
if (y + rects.popper.height > windowHeight) {
y = windowHeight - rects.popper.height;
}
return {x, y};
},
};
}; Modifiers are functions that take self-modifiers, aka options, as an array of featuresThis enables tree-shaking at a far more modular level than import {position} from '@popperjs/core';
import {offset, skiddingDistanceTuple} from '@popperjs/core/offset';
import {preventOverflow, tether} from '@popperjs/core/preventOverflow';
const {x, y} = position(reference, popper, {
placement: 'bottom',
modifiers: [
offset([skiddingDistanceTuple([0, 10])]),
preventOverflow([
tether({
offset: {
mainAxis: 10,
altAxis: 0,
},
}),
]),
],
}); Example implementation of the const skiddingDistanceTuple = (tuple) => ({ placement, rects, options }) =>
distanceAndSkiddingToXY(placement, rects, tuple);
const offset = (features) => {
return {
name: 'offset',
getOffsets({placement, rects, offsets}) {
let modifiedOffsets = {x: 0, y: 0};
features.forEach((feature) => {
modifiedOffsets = feature({placement, rects, offsets: modifiedOffsets});
});
return {
x: offsets.x + modifiedOffsets.x,
y: offsets.y + modifiedOffsets.y,
};
},
};
}; Based on this type of architecture the new slogan could be As I work on more modifiers, I might need to change some ideas here, but I think this more pure/functional way to operate is more powerful, extensible and will be less buggy. As far as the DOM splitting goes, I still need to explore this. |
Beta Was this translation helpful? Give feedback.
-
After further iteration, I realized the modifiers could be simplified even further. Now, every "feature" is really its own modifier. import {position, dom, shiftMainAxis, shiftTether} from '@popperjs/core';
const button = document.querySelector('#button');
const tooltip = document.querySelector('#tooltip');
const {x, y} = position(button, tooltip, {
platform: dom,
modifiers: [shiftMainAxis(), shiftTether()],
});
Object.assign(tooltip.style, {
position: "absolute",
top: `${x}px`,
left: `${y}px`,
}); This modularizes v2's options into their own modifiers so they can be tree-shaken away.
export default {
getReferenceRect: getCompositeRect,
getPopperRect: getLayoutRect
}; Inside const referenceRect = platform.getReferenceRect({
element: reference,
context: popper,
strategy: options.strategy,
});
const popperRect = platform.getPopperRect({
element: popper
}); I've never used React Native, so I have no idea if the object being passed as an argument is sufficient for it to get the "rects" in the native platform. This is something that needs to be tested. I've made it an object so it can be extended if necessary. Edit: started reading RN docs. It looks like our import React, { useEffect, useState } from "react";
import { View, Text } from "react-native";
const platform = {
getPopperRect: ({ element }) => element,
getReferenceRect: ({ element }) => element,
};
const Test = () => {
const [referenceRect, setReferenceRect] = useState(null);
const [popperRect, setPopperRect] = useState(null);
useEffect(() => {
if (!referenceRect || !popperRect) {
return;
}
const { x, y } = position(referenceRect, popperRect, { platform });
// ...
}, [referenceRect, popperRect]);
return (
<>
<View
onLayout={({ nativeEvent }) => setReferenceRect(nativeEvent.layout)}
>
<Text>Reference Box</Text>
</View>
<View onLayout={({ nativeEvent }) => setPopperRect(nativeEvent.layout)}>
<Text>Popper Box</Text>
</View>
</>
);
};
export default Test; If we use refs instead (probably have to to consider the layout stuff like import React, { useEffect, useRef } from "react";
import { View, Text } from "react-native";
const platform = {
getPopperRect: ({ element }) => new Promise((resolve) => element.measure(resolve)),
getReferenceRect: ({ element }) => new Promise((resolve) => element.measure(resolve)),
};
const Test = () => {
const referenceRef = useRef();
const popperRef = useRef();
useEffect(() => {
async function getCoords() {
const { x, y } = await position(referenceRef.current, popperRef.current, { platform });
// ...
}
getCoords();
}, [referenceRef, popperRef]);
return (
<>
<View ref={referenceRef}>
<Text>Reference Box</Text>
</View>
<View ref={popperRef}>
<Text>Popper Box</Text>
</View>
</>
);
};
export default Test; |
Beta Was this translation helpful? Give feedback.
-
@FezVrasta made some comments about cutting a potential release just to refresh the build setup (mainly, just use
.mjs
for/lib
&esm
), and it got me thinking about a potential direction for the library which could improve it. This is entirely my own thoughts, and not @FezVrasta, and is here mainly for initial discussion.The main concern people have with Popper is size. As we need to support more edge cases, the bundle size grows, even if 99% of consumers won't even notice nor care. Sometimes we have to assess whether it's worth even fixing an edge case because of this.
Edge cases
Popper supports a lot of edge cases. A browser extension author needs to support 100% of edge cases because of the fact that their extension works on 3rd party sites they have no control over; but a developer of a particular web app with particular conventions might not need half the code used to handle those edge cases. Perfect example: #1375
Extensibility
A lot of the size of Popper is the core used to handle ordering modifiers, phases, setting up the instance, etc. This is great for extensibility but based on the actual usage of this extensibility, it seems minimal on npm. Mostly it's used to hack something on, or fix some issue. People just use what's in core; there don't seem to be any community modifiers.
Modifiers
Modifiers could be more modular by forgoing an
options
object for functionality plugins that create the options, which can be tree-shaken away.Proposal
I want to preface this by saying I think a stateful, works out-of-the-box
createPopper
should remain in the package. This is true for libs like Bootstrap that want to abstract Popper away and focus less on the low level parts of Popper for their consumers. But if Popper really wants to go super low level, I think this is the way to go. Maybe this could also enable React Native support better?! 👀Largely I was inspired by the purity of
Popmotion
, compared to sayanime.js
. This makes Popper go more in the direction of the former.There's no state, not even
eventListeners
orapplyStyles
, nor handling of scrolling parents,offsetParent
, etc. It's 100% pure, and there are no modifiers — yet. Instead, everything is incrementally adoptable.What about DOM handling? Splitting this out could better allow us to investigate supporting React Native.
Some modifier options are unused/irrelevant for some situations. This means you create the modifier by passing it plugins to create the functionality (or options) — once again enabling tree-shaking.
Further, modifiers themselves are now extensible; you need to replace the whole modifier with the current architecture.
The biggest benefit here as mentioned is size. You only use what you need from Popper, and it's so low level that it acts more like "primitives" to build your positioning behavior.
What do you think about this type of API? Thankfully, Popper 2's codebase is in a good state and would allow this to be explored via refactoring without too much trouble. If people are interested in this direction, I can begin investigating. ✌️
Beta Was this translation helpful? Give feedback.
All reactions