dhis2 / notes

:memo: Memos, proposals, agendas and meeting minutes
19 stars 8 forks source link

Icon library structure (ui-icons) #175

Closed cooper-joe closed 3 years ago

cooper-joe commented 4 years ago

I'd like to look at the requirements from a ui-icons library. Most core apps are using Material-UI icons and we'd like to replace all these with icons provided by ui.

The icon library will contain icons that are available in different variants based on certain attributes.

For example, let's look at the icons provided for alert:

Taking alert as a single icon/entity/concept, you could say it has the following properties:

Graphically, this looks like: image

and exported as SVG files, it looks like:

icon-outlined-12.svg
icon-outlined-16.svg
icon-outlined-24.svg
icon-outlined-32.svg
icon-filled-12.svg
icon-filled-16.svg
...

Scaling a single SVG is not an option because icons are often made specifically for a set size (more detail in large icons, less detail in smaller is the usual reason). This ensures we have clear icons of all sizes. Not all icons will include all sizes.


@varl and I discussed how we could bundle a single <Icon> component and include variant, color and size as props. The idea was that any icon could be used like so:

<Icon name="alert" variant="outlined" size="12" color="red700"/>

This is a nice, clean way to call any icon. However, I couldn't figure out a way of creating this <Icon> component without including an import for every single variation of all SVG files available. This would result in a component with potentially thousands of import statements.

Suggestion

Rather than having an <Icon> component that requires all these imports, maybe it best to just have all the icons exported as components. This requires the work of setting up/exporting a lot of components, but it seems like the simplest solution.

A single icon component, IconInfoFilled32.js would look like:

const IconInfoFilled32 = ({color}) => (
  <svg fill={color}>
    ...
  </svg>
)

Feedback on this idea would be great, @dhis2/team-apps. I am planning to have enough icons finished so that all Material-UI icon usage can be replaced in 2.35.

Here are a few points I'm wondering about:

ismay commented 4 years ago

Rather than having an component that requires all these imports, maybe it best to just have all the icons exported as components. This requires the work of setting up/exporting a lot of components, but it seems like the simplest solution.

What would make sense to me is to use what you specified in the name prop as the name of the component, and use props for the rest:

// instead of
import { Icon } from '@dhis2/ui'

<Icon name="alert" variant="outlined" size="12" color="red700"/>

// we could do this
import { AlertIcon } from '@dhis2/ui'

<AlertIcon variant="outlined" size="12" color="red700"/>

Scaling a single SVG is not an option because icons are often made specifically for a set size (more detail in large icons, less detail in smaller is the usual reason). This ensures we have clear icons of all sizes. Not all icons will include all sizes.

I do think scaling can be nice to have though, I've always found that a nice bonus of using vector instead of raster graphics. I see your point though, and it does fit with our approach in other areas. I could also see an intermediate solution, with min and max-widths, so it can at least scale a bit. But that's just an idea.

if we set color: inherit; on the SVG fill, is there any way of setting a default fill color? I'm thinking it would be useful to set a reasonable default, but I'm not sure how that interferes with the inherit rule.

Since the icons are react components anyway, we could also handle this in js. Set a default and accept a fill or color prop to override it.

not all icons will have an outlined/filled variant, sometimes an icon might have a single variant, usually outlined. Does that mean that 'outlined' should be omitted and considered the default style? Then 'filled' or 'bold' or 'invert' would simply be add-on styles.

Yeah, we could have a variant prop like in your example, that accepts a string that specifies what style the icon will be rendered as (if these styles are mutually exclusive). And then we could set a default for that prop, say outlined. So <AlertIcon variant="filled" /> or <AlertIcon variant="outlined" />, etc.

Mohammer5 commented 4 years ago

I'd like to add an opinion I've state here already:

I think it'd be good to give the icons unique names, they don't need to be easily "human" readable, as long as they're unique (e. g. D2ICON1, D2ICON2, etc).

This way we can add as many "arrow" as we want and re-use very specific types in our components, just like we re-use the colors.500 as theme.error color (red500 -> error -> Field uses error for displaying an error). Then we can easily switch out icons in the intermediate stage while not having to repeat ourselves (otherwise an icon library is kind of senseless?): D2ICON1->core/Node/Arrow->core/Node. Changing the arrow of the Node won't impact any other component and D2ICON1` can safely be re-used, as long as we never modify but deprecate existing icons and add new ones if we want to entirely replace an icon. . . .

@Mohammer5 Interesting. Icons have been requested to be externally available too, e.g. from Apps.

Does that change anything or do you still think the non-human readable icon names is the way to go?

I'm used to generic names so people don't interpret too much, but rather look at the styleguide for when to use which icon (i. e. when looking for an arrow pointing right and there are several available but the styleguide has rules for them as they should only be used in a certain context)

I still think that this is applicable. It has already happened to us: There are different arrow icons in the DropdownButton and in the Node compnents, but as they were shared in the past, when the DropdownButton's icon changed, the Node's icon change unintentionally as well and even broke the cypress tests (which is why we noticed it, otherwise it would've gone unnoticed for quite a while)


I also agree with what @ismay wrote, it'd work with the above approach as well:

import { AlertIcon } from '@dhis2/ui'

<DHIS2ArrowA1
  variant="outlined"
  size="12"
  color="red700"
/>
cooper-joe commented 4 years ago

Thanks for the feedback!

I do think scaling can be nice to have though, I've always found that a nice bonus of using vector instead of raster graphics

Agreed, I do think we should adapt to situations where icons are used at the 'wrong' size, but in general iconography should be used at intended sizes to communicate efficiently, so I want to focus on that use case.

There are different arrow icons in the DropdownButton and in the Node compnents, but as they were shared in the past, when the DropdownButton's icon changed, the Node's icon change unintentionally

I think the main purpose of a DHIS2 icon library is to provide icons for use in apps for all developers (internal and external) rather than providing icons for core components. I'd argue that we should potentially bundle important/critical icons directly with components, rather than add a dependency to ui-icons that could change for reasons not related to a component.

External developers especially will not always have a styleguide/designed spec to work with, so I think reasonable names could help correct icon usage. That said, there are certainly ambiguous icons that could be used for multiple purposes - so naming those is a challenge.

What would make sense to me is to use what you specified in the name prop as the name of the component, and use props for the rest:

// instead of
import { Icon } from '@dhis2/ui'

<Icon name="alert" variant="outlined" size="12" color="red700"/>

// we could do this
import { AlertIcon } from '@dhis2/ui'

<AlertIcon variant="outlined" size="12" color="red700"/>

I think this is much more elegant, but does this not also run into the issue of loading multiple, unnecessary SVG files if only a single variant/size is used?

I did some reading about lazy-loading/code-splitting, but this is still unclear to me. If <AlertIcon> contains 12 different icons, that means 12 different SVG files. When someone does import { AlertIcon }..., with the intention of only using variant="outlined" size="12", do they not also load the all the SVG code for the 11 other SVGs? If an app used 20 icons in a single variant/size, this would be 220 unnecessary loads.

Mohammer5 commented 4 years ago

Actually, I wouldn't mind having individual components.

The variant prop kind of means that a different component will be displayed and if the size will have an impact on the underlying svg, then the size also indicates different components.

ismay commented 4 years ago

I think this is much more elegant, but does this not also run into the issue of loading multiple, unnecessary SVG files if only a single variant/size is used?

I wouldn't worry about that too much. Optimising that shouldn't block a nice DX. My priority would be to settle on a nice api first, and from there we could potentially optimise. Svg's are not that big, and if there's a lot of duplication gzip would also be able to compress it quite well.

HendrikThePendric commented 4 years ago

One important aspect that has come up a lot before, but has not been mentioned yet, is the padded VS unpadded SVG discussion. In principle I think that SVGs should not have any padding, but @cooper-joe provided a very valid explanation why he thinks they should:

[...] including padding means there is more flexibility in the icon composition (e.g. you can design to 18x16 if the extra 2 pixels help. Otherwise you’d need to go down to 16x14 to get that ratio. image been testing this 12, 16, 24, 32 icon size grid system but yeah, the padding thing can be annoying sometimes and produce unexpected results

The reason why I suggested having unpadded SVGs was mainly because with padded SVGs the alignment can look a bit funky.

Do you think we need a padded prop (or alternatively dense)?


In the past I worked on a dashboard application for over a year that was basically just a bunch of animated SVGs. In that project we were doing a lot of manual manipulation to SVG shapes, and I was quite amazed with how much is possible. So I am going to suggest a completely different approach below. I don't necessarily suggest that we should definitely go this way for our icon library, but I think it's good to at least highlight an alternative approach.

An SVG has the following the characteristics:

So, as an alternative to producing a shedload of individual SVGs that will carry a lot of duplications, we could also go the complete opposite direction and produce a much smaller set of hand-crafted SVGs that are tweaked using various techniques:

We are actually already using the same / a similar approach as the above for some of the icons we have already, such as the Switch.

The advantage of this type of approach is that you end up with a much more manageable set of components. The downside would be that every icon component would require a lot more attention than just exporting an SVG and putting it in a component.

cooper-joe commented 4 years ago

So, as an alternative to producing a shedload of individual SVGs that will carry a lot of duplications, we could also go the complete opposite direction and produce a much smaller set of hand-crafted SVGs that are tweaked using various techniques

The advantage of this type of approach is that you end up with a much more manageable set of components. The downside would be that every icon component would require a lot more attention than just exporting an SVG and putting it in a component.

This is not my area of expertise, but I think that in practice we are going to have too many icons and too few devs to make this work. Often I'll make an icon on-demand for an app that needs it, so I think we might end up with a queue of 'waiting for processing' icons.


Do you think we need a padded prop (or alternatively dense)?

This one is still keeping me up at night! I'm working through icons in the library, seeing how padded vs. not-padded would work. Right now padded is still my gut because of the reasoning you posted. I hope that we don't need a padded prop. Perhaps certain components that use icons might have some kind of padding built-in (Chip, MenuItem).


@amcgee — what do you think of this thread? (Not just the above two points, but the entire 'how should we structure icons in ui?' question 😉 )

HendrikThePendric commented 4 years ago

This is not my area of expertise, but I think that in practice we are going to have too many icons and too few devs to make this work.

Yeah that is my gut feeling too. I think the only way for my suggestion to work is if we would work out a single workflow for all icons. I'll provide an imaginary example:

I guess the only way to find out if something like the above would be possible is to just start producing SVGs, and once we've got a bunch of them, see if there are enough commonalities to start working like that.

HendrikThePendric commented 4 years ago

I've had a look at some approaches and tooling for producing consistent and optimised SVGs:

cooper-joe commented 4 years ago

I have designed the first ~100 icons that can replace the various icons that are in use across DHIS2 apps today with the aim of consistent icon use. Ideally we could get ui-icons up and running with this initial batch.

There are some modifications to the original spec I discussed above, the icons now only need 2 props: size and color. I think the -filled modifier can instead be separate icons - there are a very small number of icons that need this modifier, so I think it's simpler to just split them. See below for a visual overview of the icons ready for inclusion. Only sizes S and M are ready right now, XS and L will be added as needed.

@amcgee, what do you think about how this library could be structured, and could we get a first draft of it to include in any apps for 2.35?

image

HendrikThePendric commented 4 years ago

Good stuff @cooper-joe. Let's wait for @amcgee before we take any action, but in the meanwhile I'd like to share my thoughts:

So I'm thinking that we can probably use some code generating script in this project to avoid us from manually editing 100s of files. Provided the SVGs are exported in a consistent manner, and with consistent file names, i.e. iconName_size, and all/most SVGs need the same styles, we can probably write a script what wraps these SVGs into a small template so they are available as a react component with the right props....