Skip to content
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

Adds JLIcon, an easier way to consume icons as SVG nodes #7299

Closed
wants to merge 41 commits into from

Conversation

telamonian
Copy link
Member

@telamonian telamonian commented Oct 7, 2019

References

Followup to #6034, #7192, #7266, and a bunch of others

Code changes

Working on the one true way to consume icons in jlab, with a goal to get this in by v2.0. The basic design plan is this:

  • Create a wrapper that will take the raw contents of an .svg file (as a string) and return a React.Component class
  • There will be one component class per icon (ie per .svg file)
  • Each component class will also have a static method to allow for the use of it's specific icon outside of jsx/react
  • Creating new icon classes will be as easy as importing your .svg then making a call to the wrapper

User-facing changes

None

Backwards-incompatible changes

Will remove all other means of consuming icons. In exchange, will add a super easy way to setup/consume theme-aware icons in extensions.

@jupyterlab-dev-mode
Copy link

Thanks for making a pull request to JupyterLab!

To try out this branch on binder, follow this link: Binder

@telamonian telamonian changed the title starting work on JLIcon, to be used as an icon class wrapper [WIP] The one true way to setup/consume icons in Jupyterlab Oct 7, 2019
@telamonian telamonian added this to the 2.0 milestone Oct 7, 2019
@telamonian
Copy link
Member Author

@saulshanabrook Saul said:

Another option is we could move from having a IconRegistry to just exporting each icon as a typescript constant. We could have an Icon class and instantiate it for each icon, for something like:

import runningSVG form './icons/running.svg'
# iconSvg is the text source of the icon. We could setup webpack to always load SVG with the >`raw-loader`
export const runningIcon = new Icon(runningSVG)

Then in another file you could import the icon and get the react component or HTML node creator off of it. This would let us rely on TypeScript to make sure we define each icon we want and also it eliminates the need to learn a new concept of an IconRegistry.

In this case, we would also remove the icon extension.

The design goal of this PR can be thought of as a riff on Saul's suggestion.

@telamonian
Copy link
Member Author

telamonian commented Oct 10, 2019

The return type of createIcon is the union of the return type of React.forwardRef plus whatever static methods that I add (currently just element). I initially tried to declare the return type as IComponent, using typeof and ReturnType to capture the return type of forwardRef:

  export interface IComponent extends ReturnType<typeof React.forwardRef<HTMLDivElement, JLIcon.IProps>> {
    element?: (props: IProps) => HTMLElement;
  }

but that's not valid syntax, due to the vagaries of the current typeof implementation. You can get something that is valid syntax by assigning a type alias to the result of typeof:

  type _ret<T, P> = typeof React.forwardRef;
  export interface IComponent extends ReturnType<_ret<HTMLDivElement, JLIcon.IProps>> {
    element?: (props: IProps) => HTMLElement;
  }

but for reasons I don't understand, the concrete template parameters in ReturnType<_ret<HTMLDivElement, JLIcon.IProps>> don't actually end up getting bound correctly to the resulting type. Finally, I just gave up and copied the return type of forwardRef directly from @types/react:

  export interface IComponent extends ForwardRefExoticComponent<PropsWithoutRef<IProps> & RefAttributes<HTMLDivElement>> {
    element?: (props: IProps) => HTMLElement;
  }

This works well, though it seems kind of fragile (wrt changes to @types/react). The issue here has something to do with using both typeof and ReturnType on a generic function, so if anyone has any insight into the inner workings of Typescript in this case it would be most welcome.

@sccolbert
Copy link
Contributor

I think I like Saul's quoted idea above. Create an Icon class, that has some methods on it which return the content in various forms. One method could be component() which returns the react component. That will be easier to specify the typing, and more flexible in the future should you decide you need to consume the icon another way.

<span>myIcon.component()</span>

@telamonian
Copy link
Member Author

telamonian commented Oct 11, 2019

Whelp, apparently I was under some mistaken impressions about valid syntax for JSX component names; I thought they had to start with an uppercase letter and have no dot. In actual fact, the only invalid names seem to be those that start with a lowercase letter but don't have a dot:

const bar = TrustedIcon;
const Bar = TrustedIcon;

const foo: any = {};
const Foo = foo;
foo.icon = TrustedIcon;
foo.Icon = TrustedIcon;

const barFunc = () => TrustedIcon;
const BarFunc = () => TrustedIcon;

foo.iconFunc = () => TrustedIcon;
foo.IconFunc = () => TrustedIcon;

return (
  <div style={{display: "inline-flex"}}>
    <bar/>       {/* error! */}
    <Bar/>       {/* valid */}

    <foo.icon/>  {/* valid */}
    <foo.Icon/>  {/* valid */}
    <Foo.icon/>  {/* valid */}
    <Foo.Icon/>  {/* valid */}

    {/* can't use a function call as a JSX component */}

    <barFunc()/>       {/* error! */}
    <BarFunc()/>       {/* error! */}

    <foo.iconFunc()/>  {/* error! */}
    <foo.IconFunc()/>  {/* error! */}
    <Foo.iconFunc()/>  {/* error! */}
    <Foo.IconFunc()/>  {/* error! */}

    {/* but you can use function calls if you escape from JSX */}

    {barFunc()}                                           {/* valid */}
    {React.createElement(barFunc(), {className: "bar"})}  {/* valid */}
  </div>

Function calls are never valid JSX components, but you can use them if you escape from JSX.

@telamonian
Copy link
Member Author

@sccolbert I'll take your suggestion (to take Saul's original suggestion) and rearrange JLIcon into a class

@telamonian
Copy link
Member Author

telamonian commented Oct 11, 2019

@saulshanabrook @sccolbert Thank you for your feedback. It helped me to figure out how to vastly simplify the design of the new icon objects and still get the ease-of-use in JSX that I was chasing after.

In the latest changes that I just pushed, all of the icon objects are now instances of the JLIcon class. JLIcon instances have an .element method that returns an HTMLElement, and a react property that holds a React component. Here's an example of a JLIcon instance being used in JSX/React:

if (props.allCellsTrusted) {
return <trustedIcon.react top={'2px'} kind={'statusBar'} />;
} else {
return <notTrustedIcon.react top={'2px'} kind={'statusBar'} />;
}

Though the internals of JLIcon need some work (and possibly its interface needs a few tweaks as well), I'm happy with the current direction. It completely eliminates the need for the convoluted type declarations of the previous design.

The next thing for me to tackle will be the non-React use cases for JLIcon, including figuring out the best way to integrate it with Phosphor.

also had to revert the cleanup of createIcon's return type. The new
way didn't work since some template parameters weren't specified
also added explicit types to the call to `React.forwardRef`
within `createIcon`
individual icons are now instances of JLIcon class
more efficient, since this way the component returned by .react is
created only once (as part of the constructor) and cached. Need to be
careful with future changes to JLIcon.constructor, as execution order
of initializers is now important
@telamonian telamonian changed the title [WIP] The one true way to setup/consume icons in Jupyterlab Adds JLIcon, an easier way to consume icons as SVG nodes Dec 25, 2019
@telamonian
Copy link
Member Author

The JLIcon class is finished, so I think this PR is as well. There's more work to do in terms of switching everything over to JLIcon and cleaning up several layers worth of old icon API, but I think it would be best to get JLIcon merged in and then follow up with another PR.

Before this can be pulled in we'll need to do another lumino release, since this PR relies on unreleased changes in the latest master version of @lumino/virtualdom. @blink1073 Is there anything I can do to help out with that?

@saulshanabrook
Copy link
Member

I will review this today! Thanks @telamonian for all your work here.

@jasongrout jasongrout mentioned this pull request Dec 30, 2019
25 tasks
@telamonian
Copy link
Member Author

Thanks @saulshanabrook!

If you actually want to test the code from this PR, you'll also need to yarn link in the latest master of Lumino, with the changes from jupyterlab/lumino#36. The yarn link instructions are here. I ended up needing to link in 4 packages to get everything to work correctly:

  • @lumino/commands
  • @lumino/messaging
  • @lumino/virtualdom
  • @lumino/widgets

@saulshanabrook
Copy link
Member

@telamonian Great, I will have a go at running it locally. Looks good overall! I wanna do some tests around the React component stuff... Curious if I can factor out the component into an external function.

@telamonian
Copy link
Member Author

@saulshanabrook Looking over your comments so far, it might be better if you just take a look at #7700. It's the direct continuation of this PR, and already has resolutions for most the things you pointed out in the review. For example, I dropped the use of render/unrender from listings.

@jasongrout
Copy link
Contributor

Since #7700 includes this PR and important fixes for this PR, should we just close this PR in deference to that one?

@telamonian
Copy link
Member Author

Since #7700 includes this PR and important fixes for this PR, should we just close this PR in deference to that one?

@jasongrout I think that's fine. I originally split this into two PRs in order to make it easier to read over and review the basic code for JLIcon. I personally find it very difficult to review any PR that balloons over the ~1000 lines mark. Not sure if this approach actually made it any easier, since I'm not really in a position to accept changes on this PR.

So as long as someone is up to reviewing #7700 as one big piece (it currently has changes to 239 files and over 4000 lines), feel free to close #7299

@jasongrout
Copy link
Contributor

jasongrout commented Jan 1, 2020

@saulshanabrook - since you are reviewing this (thanks again!), how about you make the decision about whether to review this PR or close it and just look at #7700.

@saulshanabrook
Copy link
Member

Let's close this and just look at that one.

@lock lock bot added the status:resolved-locked Closed issues are locked after 30 days inactivity. Please open a new issue for related discussion. label Jan 31, 2020
@lock lock bot locked as resolved and limited conversation to collaborators Jan 31, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement pkg:ui-components status:resolved-locked Closed issues are locked after 30 days inactivity. Please open a new issue for related discussion.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants