mckinsey / vizro

Vizro is a toolkit for creating modular data visualization applications.
https://vizro.readthedocs.io/en/stable/
Apache License 2.0
2.47k stars 109 forks source link

Can I add an icon to a button? #408

Closed cverluiseQB closed 3 weeks ago

cverluiseQB commented 3 months ago

Which package?

vizro

What's the problem this feature will solve?

As a developer, I would like to easily communicate the purpose of a button using standard icons.

Describe the solution you'd like

A simple option would be to add an icon field to the vm.Button here a valid material icon name could be passed (similar to what is done for nav) so that the icon is natively rendered at the left of the text of the button

Alternative Solutions

Document how this can be done with custom component (e.g. pointing at the code of the export data button).

Additional context

Nothing to add

Code of Conduct

huong-li-nguyen commented 2 months ago

Hey @cverluiseQB,

yes, there is! Generally, whenever you find yourself in a situation where you want to extend the functionality of an existing component, creating custom components is the answer 👍

I want to emphasize two additional changes, though:

.text-with-icon {
  display: flex;
  gap: 4px;
}

.text-with-icon .material-symbols-outlined {
  color: unset;
}

Example

from typing import Literal

import vizro.models as vm
from dash import HTML
from vizro import Vizro

class CustomButton(vm.Button):
    """Custom Button that allows for an icon."""

    type: Literal["custom_button"] = "custom_button"
    icon: str = "Download"

    def build(self):
        button_build = super().build()
        button_build[self.id].children = html.Div(
            [html.Span(self.icon, className="material-symbols-outlined"), self.text], className="text-with-icon"
        )
        return button_build

vm.Page.add_type("components", CustomButton)

page = vm.Page(title="Custom Button", components=[CustomButton(text="Export Data", icon="Download")])
dashboard = vm.Dashboard(pages=[page])

Vizro().build(dashboard).run()

Screenshot 2024-04-05 at 16 36 35

antonymilne commented 2 months ago

This is a great question @cverluiseQB, thanks for posting it here so we have it on the open source repo 👍 And great answer @huong-li-nguyen 💯

A couple of notes for future reference: this is something that dmc natively enables through dash-iconify and e.g. dmc.Button with leftIcon and rightIcon arguments. I don't think we want to have that much flexibility on all our components but a new icon field that handles an icon using Google material library just like @huong-li-nguyen suggests sounds like the right solution here if it is something that we want to add.

Another thing I've wondered before is whether we should just encourage users who want to do such things to relax the field types. e.g. vm.Button has text: str but if you relaxed that to Any then you could directly do this:

class CustomButton(vm.Button):
    """Custom Button that allows for an icon."""

    type: Literal["custom_button"] = "custom_button"
    text: Any

CustomButton(text=html.Div([html.Span("Download", className="material-symbols-outlined"), "Export Data"], className="text-with-icon"))

(completely untested, might not work, but hopefully you get the general idea)

The advantage of this is that it has infinite flexibility since you could put whatever you like in text then - i.e. put the icon wherever you like inside the label, put in any sort of HTML, etc. For example, previously I wanted to put text formatted as code into a page title using html.Pre and it was awkward because of our title: str limitation.

The disadvantage is that the model becomes much less prescriptive. On the Vizro side it would be much cleaner to have a new icon field than just allow arbitrary content in text. So I don't want us to do this in Vizro itself, but depending on how much flexibility the user requires it might sometimes be a good solution for writing a custom component.