Skip to content

Commit

Permalink
Add icon to st.download_button (streamlit#9322)
Browse files Browse the repository at this point in the history
Adding the `icon` param to `st.download_button` to allow for emojis/material icons to be added to button label
  • Loading branch information
mayagbarnes authored Aug 26, 2024
1 parent bec7ff1 commit 4f1b7ef
Show file tree
Hide file tree
Showing 19 changed files with 89 additions and 8 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions e2e_playwright/st_download_button.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@
type="primary",
)

st.download_button(
"Button with emoji icon",
data="Hello world!",
icon="⬇️",
)

st.download_button(
"Button with material icon",
data="Hello world!",
icon=":material/download:",
)

random_str = str(random())
clicked = st.download_button(label="Download random text", data=random_str)

Expand Down
14 changes: 7 additions & 7 deletions e2e_playwright/st_download_button_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def test_download_button_widget_rendering(
):
"""Test that download buttons are correctly rendered via screenshot matching."""
download_buttons = themed_app.get_by_test_id("stDownloadButton")
expect(download_buttons).to_have_count(9)
expect(download_buttons).to_have_count(11)

assert_snapshot(download_buttons.nth(0), name="st_download_button-default")
assert_snapshot(download_buttons.nth(1), name="st_download_button-disabled")
Expand All @@ -35,6 +35,8 @@ def test_download_button_widget_rendering(
download_buttons.nth(5), name="st_download_button-use_container_width_help"
)
assert_snapshot(download_buttons.nth(6), name="st_download_button-primary")
assert_snapshot(download_buttons.nth(7), name="st_download_button-emoji_icon")
assert_snapshot(download_buttons.nth(8), name="st_download_button-material_icon")


def test_show_tooltip_on_hover(app: Page, assert_snapshot: ImageCompareFunction):
Expand All @@ -44,22 +46,20 @@ def test_show_tooltip_on_hover(app: Page, assert_snapshot: ImageCompareFunction)


def test_value_correct_on_click(app: Page):
download_buttons = app.get_by_test_id("stDownloadButton")
expect(download_buttons).to_have_count(9)
download_button = app.get_by_test_id("stDownloadButton").nth(7).locator("button")
download_button = app.get_by_test_id("stDownloadButton").nth(9).locator("button")
download_button.click()
expect(app.get_by_test_id("stMarkdown").first).to_have_text("value: True")


def test_value_not_reset_on_reclick(app: Page):
download_button = app.get_by_test_id("stDownloadButton").nth(7).locator("button")
download_button = app.get_by_test_id("stDownloadButton").nth(9).locator("button")
download_button.click()
download_button.click()
expect(app.get_by_test_id("stMarkdown").first).to_have_text("value: True")


def test_click_calls_callback(app: Page):
download_button = app.get_by_test_id("stDownloadButton").nth(8).locator("button")
download_button = app.get_by_test_id("stDownloadButton").nth(10).locator("button")
expect(app.get_by_test_id("stMarkdown").nth(3)).to_contain_text(
"Download Button was clicked: False"
)
Expand All @@ -73,7 +73,7 @@ def test_click_calls_callback(app: Page):


def test_reset_on_other_widget_change(app: Page):
download_button = app.get_by_test_id("stDownloadButton").nth(8).locator("button")
download_button = app.get_by_test_id("stDownloadButton").nth(10).locator("button")
download_button.click()
expect(app.get_by_test_id("stMarkdown").nth(1)).to_have_text("value: True")
expect(app.get_by_test_id("stMarkdown").nth(2)).to_have_text(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,4 +170,18 @@ describe("DownloadButton widget", () => {
expect(downloadButton).toHaveStyle("width: 100%")
})
})

it("renders an emoji icon if provided", () => {
render(<DownloadButton {...getProps({ icon: "😀" })} />)

const icon = screen.getByTestId("stIconEmoji")
expect(icon).toHaveTextContent("😀")
})

it("renders a material icon if provided", () => {
render(<DownloadButton {...getProps({ icon: ":material/thumb_up:" })} />)

const icon = screen.getByTestId("stIconMaterial")
expect(icon).toHaveTextContent("thumb_up")
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,20 @@

import React, { ReactElement } from "react"

import { useTheme } from "@emotion/react"

import { DownloadButton as DownloadButtonProto } from "@streamlit/lib/src/proto"
import BaseButton, {
BaseButtonKind,
BaseButtonSize,
BaseButtonTooltip,
} from "@streamlit/lib/src/components/shared/BaseButton"
import { DynamicIcon } from "@streamlit/lib/src/components/shared/Icon"
import { WidgetStateManager } from "@streamlit/lib/src/WidgetStateManager"
import StreamlitMarkdown from "@streamlit/lib/src/components/shared/StreamlitMarkdown"
import { StreamlitEndpoints } from "@streamlit/lib/src/StreamlitEndpoints"
import { LibContext } from "@streamlit/lib/src/components/core/LibContext"
import { EmotionTheme } from "@streamlit/lib/src/theme"

export interface Props {
endpoints: StreamlitEndpoints
Expand Down Expand Up @@ -54,6 +58,7 @@ export function createDownloadLink(
}

function DownloadButton(props: Props): ReactElement {
const { colors }: EmotionTheme = useTheme()
const { disabled, element, widgetMgr, width, endpoints, fragmentId } = props
const style = { width }
const {
Expand Down Expand Up @@ -81,6 +86,9 @@ function DownloadButton(props: Props): ReactElement {
// we need to pass the container width down to the button
const fluidWidth = element.help ? width : true

// Material icons need to be larger to render similar size of emojis, emojis need addtl margin
const isMaterialIcon = element.icon.startsWith(":material")

return (
<div
className="stDownloadButton"
Expand All @@ -95,6 +103,14 @@ function DownloadButton(props: Props): ReactElement {
onClick={handleDownloadClick}
fluidWidth={element.useContainerWidth ? fluidWidth : false}
>
{element.icon && (
<DynamicIcon
size={isMaterialIcon ? "lg" : "base"}
margin={isMaterialIcon ? "0 sm 0 0" : "0 md 0 0"}
color={colors.bodyText}
iconValue={element.icon}
/>
)}
<StreamlitMarkdown
source={element.label}
allowHTML={false}
Expand Down
26 changes: 25 additions & 1 deletion lib/streamlit/elements/widgets/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ def download_button(
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
type: Literal["primary", "secondary"] = "secondary",
icon: str | None = None,
disabled: bool = False,
use_container_width: bool = False,
) -> bool:
Expand Down Expand Up @@ -299,6 +300,23 @@ def download_button(
button with additional emphasis or "secondary" for a normal button. Defaults
to "secondary".
icon : str or None
An optional emoji or icon to display next to the button label. If ``icon``
is ``None`` (default), no icon is displayed. If ``icon`` is a
string, the following options are valid:
* A single-character emoji. For example, you can set ``icon="🚨"``
or ``icon="🔥"``. Emoji short codes are not supported.
* An icon from the Material Symbols library (rounded style) in the
format ``":material/icon_name:"`` where "icon_name" is the name
of the icon in snake case.
For example, ``icon=":material/thumb_up:"`` will display the
Thumb Up icon. Find additional icons in the `Material Symbols \
<https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded>`_
font library.
disabled : bool
An optional boolean, which disables the download button if set to
True. The default is False.
Expand Down Expand Up @@ -388,8 +406,9 @@ def download_button(
on_click=on_click,
args=args,
kwargs=kwargs,
disabled=disabled,
type=type,
icon=icon,
disabled=disabled,
use_container_width=use_container_width,
ctx=ctx,
)
Expand Down Expand Up @@ -609,6 +628,7 @@ def _download_button(
kwargs: WidgetKwargs | None = None,
*, # keyword-only arguments:
type: Literal["primary", "secondary"] = "secondary",
icon: str | None = None,
disabled: bool = False,
use_container_width: bool = False,
ctx: ScriptRunContext | None = None,
Expand All @@ -627,6 +647,7 @@ def _download_button(
"download_button",
user_key=key,
label=label,
icon=icon,
file_name=file_name,
mime=mime,
key=key,
Expand Down Expand Up @@ -655,6 +676,9 @@ def _download_button(
if help is not None:
download_button_proto.help = dedent(help)

if icon is not None:
download_button_proto.icon = validate_icon_or_emoji(icon)

serde = ButtonSerde()

button_state = register_widget(
Expand Down
14 changes: 14 additions & 0 deletions lib/tests/streamlit/elements/download_button_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ def test_just_label(self, data):
self.assertEqual(c.type, "secondary")
self.assertEqual(c.disabled, False)

def test_emoji_icon(self):
"""Test that it can be called with emoji icon."""
st.download_button("the label", icon="⚡", data="juststring")

c = self.get_delta_from_queue().new_element.download_button
self.assertEqual(c.icon, "⚡")

def test_material_icon(self):
"""Test that it can be called with material icon."""
st.download_button("the label", icon=":material/thumb_up:", data="juststring")

c = self.get_delta_from_queue().new_element.download_button
self.assertEqual(c.icon, ":material/thumb_up:")

def test_just_disabled(self):
"""Test that it can be called with disabled param."""
st.download_button("the label", data="juststring", disabled=True)
Expand Down
1 change: 1 addition & 0 deletions proto/streamlit/proto/DownloadButton.proto
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ message DownloadButton {
bool disabled = 7;
bool use_container_width = 8;
string type = 9;
string icon = 10;
}

0 comments on commit 4f1b7ef

Please sign in to comment.