-
Notifications
You must be signed in to change notification settings - Fork 38
SVG support #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SVG support #8
Conversation
…to put that change into a separate PR This reverts commit 1647e85.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for taking a shot at this! It's quite tricky 😄. I think you're on the right track, but this isn't quite there yet.
I'm not totally sure, but I'd think it's likely that the rendering problems you're talking about with replaceChild
might be caused by the document.createElement
issue (see my comment). If you take a shot at fixing that, that might let you use replaceChild
, and then this whole thing might get simpler.
Give it a go, happy to chat or help if that'd be useful. I'd recommend testing out some of the existing HTML tests with SVGs if you want to stress test this a little and see how it behaves.
src/index.tsx
Outdated
let lastPlaceholder: Node | undefined; | ||
|
||
const portalNode = Object.assign(document.createElement('div'), { | ||
const portalNode = Object.assign(document.createElement(tagName), { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Viewing the story here in Firefox gives me a React warning:
Warning: The tag
<text>
is unrecognized in this browser. If you meant to render a React component, start its name with an uppercase letter.
I think that's because SVG elements are supposed to be created explicitly with document.createElementNS("http://www.w3.org/2000/svg", tagName)
, not with document.createElement(tagName)
. See https://stackoverflow.com/questions/39457888/after-creating-a-text-element-with-javascript-why-cant-i-getbbox and facebook/react#11899 (comment).
I'm not totally sure how React deals with this generally, but I'd guess that it has a list of SVG tag names, and calls the right method for each type. Happy to do that or something better, if you have any ideas?
Thanks for the feedback and investigation! I didn't realize things like video-in-svg were even possible. I'll do some more research and give the createElement/createElementNS stuff a try. |
I think I have it working -- I'm going to do some cleanup and update this branch/pr shortly, if the general strategy seems sound.
With those changes the svg elements are going through There is a potential breaking change: Two other notes:
|
… needs to be. This was squashed from an experimental branch
I ran into an issue with the I've prototyped out a few different options, but none of them feels like the clear solution: A) Require the caller to tell us whether or not we're dealing with SVG. This is easily done as an argument to This is an easy solution, but it feels like an undue burden on consumers -- especially since omitting the value would lead to a broken UI. B) If the This works well today, but I suspect it'll run into issues with asynchronous/interruptible rendering in the future. Putting the InPortal's C) Inspect the InPortal's This works in general, but there are a few edge cases which could be dangerous: D) If the This feels kinda dirty to me, but it works well and emits no console errors or warnings. It's definitely a coupling between the two components, though, through a non-obvious channel. It should work fine with asynchronous/interruptible rendering, I think, and there is no visible flicker in the UI, so this is the best option I've found so far for dealing with this issue. Any guidance or suggestions? (And apologies for the two walls of text; I know there's a lot here.) |
Wow, nice work! Thanks for all the investigation, this is great. I'm rushed off my feet right now, but I'll dig into this properly next week, looks like there's some promising options 👍 |
Just to float out a new thought, for when you're back: (and no hurry on any of this -- I appreciate your time) After playing around a bit more, I'd like to propose another revision to this to:
Here's the reasoning, and the issues I ran into with the current PR:
I can update this with those changes, although it may be a few days. |
Ok, this is really interesting! Great stuff. I think:
In terms of doing this in practice, rather than using tagName and inferring from there, I propose we split the package up a little to have separate methods/components for each (sharing quite a bit of code ofc) so that you use it like: import * as portals from 'react-reverse-portal';
const svgNode = portals.svg.createPortalNode();
const htmlNode = portals.html.createPortalNode();
// To define InPortals:
<portals.svg.InPortal node={svgNode} />
<portals.html.InPortal node={htmlNode} />
// To render an OutPortal:
<svg><portals.svg.OutPortal node={svgNode} /></svg>
<div><portals.html.OutPortal node={htmlNode} /></div> If you know you only want one, you can do that with: import { html as portals } from 'react-reverse-portal';
// Exactly the same API as today, works for any HTML:
const node = portals.createPortalNode(); // An HTML node
<portals.InPortal> // an HTML InPortal
<portals.OutPortal> // an HTML OutPortal
// Same thing works with import { svg as portals }, for the SVG-only case. Last, to avoid breaking changes and for general convenience, we can keep the existing API on the root package, as an alias for Advantages of all this explicitness:
Does that make sense? Am I missing anything? You've done a lot of digging into the issues, of which there are quite a few, so it's hard to be sure I've covered everything! In general I think this will work though, and be fairly easy to use too. |
Thanks! I agree that the explicitness seems best. I think I can update this PR with those changes by this weekend. The only comment I have is on the entry -- whether the element type (html/svg) is best specified via the import ( There are only two lines of code different between html mode and svg mode (the createElement at init, and the error check at runtime), and if element type is an argument then we can just set a default to avoid breaking changes ( I've been playing around with other types as well (like Math and inline text) -- they seem to only affect the same two spots as svg, although I'm not 100% certain. The potential of supporting four element types seems possible to me, though -- and it should work either as 4 importable namespaces or as 4 allowed |
Ok, great! No hurry of course, whenever you have time.
Yeah, it's quite debatable either way! If there are clear problems I'm definitely open to alternative approaches. There's a couple of reasons I'm leaning towards a fully namespaced API though:
Internally the two namespaces can still share almost all their code of course, totally agree they're very similar, it's just the exposed API that I'm thinking about here. Overall I'm not really that worried about short-term disruption. For people who don't need SVG, they don't need to upgrade at all, so they don't care. For people who do need SVG they do need to upgrade, but they need to think about their portal types anyway. I'm somewhat tempted to say we should even increase the disruption, just to avoid any confusion or compatibility issues. Instead of keeping the current API, we'd namespace everything, so no backward compatibility at all, and this PR would be the start of react-reverse-portals v2, where you must import either
Awesome! That sounds great. I'm not exactly sure what you mean about inline text here though, can you explain a little more? |
I agree this would be very annoying, but I don't think it'd be necessary because the Also, somewhat selfishly: being able to use the same
Very good point. Here's a possible third idea that might fit both: what if the import { createHtmlPortalNode, InPortal, OutPortal } from 'react-reverse-portal';
const myPortalNode = createHtmlPortalNode();
// <InPortal node={myPortalNode}> import { htmlNode, InPortal, OutPortal } from 'react-reverse-portal';
const myPortalNode = htmlNode.create();
// <InPortal node={myPortalNode}>
// with room to add other things later, like htmlNode.debug() So then the portalNode creation is always fixed and explicit -- and the existing Thoughts? As before, I'll go with whatever option you choose. Any of them will work okay for what I'm doing, in the end, but this seemed like a possible middle idea.
It's mostly just a "hey this is neat" capability I was experimenting with. :-) I was playing around with textNodes (instead of full elements) and what it might take to do a portal for text within a sentence, just to see how it might work. Because the portals currently use a I tried a few routes:
So, inline elements don't seem like a common case to me, but if anybody did need to portal around inline elements instead of block elements then making an entry point or namespace for |
I think the branch is updated with all the changes now: the The typings took a bit of work, but I think it's all good now: I used a technique I'd done once elsewhere (with the shared code using unified types that aren't exported, while exporting only the specific types for the public api) but if there's a better way I'd love to learn it. I've tested it pretty thoroughly, and everything seems to work as before. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is very cool. A couple of comments to discuss, but I'm definitely sold on the approach, and I'm happy to go for the create{Html,Svg}PortalNode
API instead of namespacing.
The one last extra thing we need to do is to update the docs for this, but then it's probably good to go I think.
Notes on sending 'direct' content through the portal: The idea and goal:
Possible benefits:
What I tried:
Problem:
Current conclusions and thoughts:
|
I think it could still work, as we can use That said, doing that without a placeholder is very difficult, especially since other sibling node could be updated & changed in the meantime. It might be nice to do that one day but for now I don't think it's a big deal, I'm happy to leave it. |
Ok, I think this all looks good to go now. Anything essential left that you're aware of? If not, I'll merge and get this out as a breaking release, and we can finally close off #2 😄 |
Looking over the diff one last time, I noticed an accidental inaccuracy in the docs I wrote, where I'd blamed the SVG issue on browsers before that last bit of research. I just pushed to 6870bb5 Unrelated to this PR, I'd prototyped portal nodes for In any case, thanks for all your help and time with this! |
Merged, tested in the HTTP Toolkit UI, added a couple of tweaks (we'd lost the portal generic types along the way), and now released as v2.0.0 🎉 I'm happy to look at a MathML etc PR too, if that'd be useful to you. Until there's somebody with a specific use case I don't think it's critical, but I have no problem with adding support for it anyway if you're keen to look into it. Thanks again for all your hard work on this. It's a great result, I'm really happy with how it's all turned out! 👍 👍 👍 |
This is an attempt to address issue #2 -- I've tested it locally, and added a storybook story for it, but I haven't tested it very extensively.
There are two noteworthy changes here:
createPortalNode
to set the element type which will be used for theportalNode
. I noticed that a few of the stories were passing this argument, but that it wasn't actually being used.innerHTML
through the portal instead of the dom elements themselves. This makes the repro example from issue Does not work with SVGs #2 work, and the innerHTML technique was suggested by several Stackoverflow threads, but I'm not sure whether it's the right way to do this.Other approaches I tried, for the second bullet point:
childNodes
through the portal (instead of the element itself, or its innerHTML). This feels like it should be the right way to support elements in contexts where you can't have a<div>
, and it did arrange everything correctly in the dom, but it still didn't render correctly.