Skip to content

Commit

Permalink
[feat] Add icon to st.link_button (streamlit#9350)
Browse files Browse the repository at this point in the history
## Describe your changes

Adds the ability to add an Icon (emoji or Material) to `st.link_button`

## GitHub Issue Link (if applicable)

## Testing Plan

- Unit Tests (JS and/or Python) ✅
- E2E Tests ✅

---

**Contribution License Agreement**

By submitting this pull request you agree that all contributions to this
project are made under the Apache 2.0 license.
  • Loading branch information
sfc-gh-bnisco authored Aug 29, 2024
1 parent 57ceda6 commit c94837e
Show file tree
Hide file tree
Showing 43 changed files with 92 additions and 1 deletion.
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.
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.
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_link_button.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,15 @@
use_container_width=True,
help="help text here",
)

st.link_button(
"Link Button with emoji icon",
"https://streamlit.io",
icon="🎈",
)

st.link_button(
"Link Button with Material icon",
"https://streamlit.io",
icon=":material/bolt:",
)
2 changes: 1 addition & 1 deletion e2e_playwright/st_link_button_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
def test_link_button_display(themed_app: Page, assert_snapshot: ImageCompareFunction):
"""Test that st.link_button renders correctly."""
link_elements = themed_app.get_by_test_id("stLinkButton")
expect(link_elements).to_have_count(6)
expect(link_elements).to_have_count(8)

for i, element in enumerate(link_elements.all()):
assert_snapshot(element, name=f"st_link_button-{i}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,20 @@ describe("LinkButton widget", () => {
expect(linkButton).toHaveStyle("width: 100%")
})
})

describe("icons", () => {
it("renders an emoji icon", () => {
const props = getProps({ icon: "🎈" })
render(<LinkButton {...props} />)

expect(screen.getByText("🎈")).toBeVisible()
})

it("renders a Material icon", () => {
const props = getProps({ icon: ":material/bolt:" })
render(<LinkButton {...props} />)

expect(screen.getByTestId("stIconMaterial")).toBeVisible()
})
})
})
15 changes: 15 additions & 0 deletions frontend/lib/src/components/elements/LinkButton/LinkButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@

import React, { MouseEvent, ReactElement } from "react"

import { useTheme } from "@emotion/react"

import { LinkButton as LinkButtonProto } from "@streamlit/lib/src/proto"
import {
BaseButtonKind,
BaseButtonSize,
BaseButtonTooltip,
} from "@streamlit/lib/src/components/shared/BaseButton"
import StreamlitMarkdown from "@streamlit/lib/src/components/shared/StreamlitMarkdown"
import { EmotionTheme } from "@streamlit/lib/src/theme"
import { DynamicIcon } from "@streamlit/lib/src/components/shared/Icon"

import BaseLinkButton from "./BaseLinkButton"

Expand All @@ -34,6 +38,9 @@ export interface Props {

function LinkButton(props: Readonly<Props>): ReactElement {
const { disabled, element, width } = props
const { colors }: EmotionTheme = useTheme()
// Material icons need to be larger to render similar size of emojis, emojis need addtl margin
const isMaterialIcon = element.icon.startsWith(":material")
const style = { width }

const kind =
Expand Down Expand Up @@ -68,6 +75,14 @@ function LinkButton(props: Readonly<Props>): ReactElement {
rel="noreferrer"
aria-disabled={disabled}
>
{element.icon && (
<DynamicIcon
iconValue={element.icon}
color={colors.bodyText}
size={isMaterialIcon ? "lg" : "base"}
margin={isMaterialIcon ? "0 sm 0 0" : "0 md 0 0"}
/>
)}
<StreamlitMarkdown
source={element.label}
allowHTML={false}
Expand Down
23 changes: 23 additions & 0 deletions lib/streamlit/elements/widgets/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,7 @@ def link_button(
*,
help: str | None = None,
type: Literal["primary", "secondary"] = "secondary",
icon: str | None = None,
disabled: bool = False,
use_container_width: bool = False,
) -> DeltaGenerator:
Expand Down Expand Up @@ -460,6 +461,23 @@ def link_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 link button if set to
True. The default is False.
Expand Down Expand Up @@ -497,6 +515,7 @@ def link_button(
help=help,
disabled=disabled,
type=type,
icon=icon,
use_container_width=use_container_width,
)

Expand Down Expand Up @@ -703,6 +722,7 @@ def _link_button(
help: str | None,
*, # keyword-only arguments:
type: Literal["primary", "secondary"] = "secondary",
icon: str | None = None,
disabled: bool = False,
use_container_width: bool = False,
) -> DeltaGenerator:
Expand All @@ -716,6 +736,9 @@ def _link_button(
if help is not None:
link_button_proto.help = dedent(help)

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

return self.dg._enqueue("link_button", link_button_proto)

def _page_link(
Expand Down
24 changes: 24 additions & 0 deletions lib/tests/streamlit/elements/link_button_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"""link_button unit tests."""

import streamlit as st
from streamlit.errors import StreamlitAPIException
from tests.delta_generator_test_case import DeltaGeneratorTestCase


Expand Down Expand Up @@ -64,3 +65,26 @@ def test_use_container_width_is_false_by_default(self):

c = self.get_delta_from_queue().new_element.link_button
self.assertEqual(c.use_container_width, False)

def test_emoji_icon(self):
"""Test that it can be called with an emoji icon."""
st.link_button("the label", url="https://streamlit.io", icon="🎈")

c = self.get_delta_from_queue().new_element.link_button
self.assertEqual(c.icon, "🎈")

def test_material_icon(self):
"""Test that it can be called with a material icon."""
st.link_button("the label", url="https://streamlit.io", icon=":material/bolt:")

c = self.get_delta_from_queue().new_element.link_button
self.assertEqual(c.icon, ":material/bolt:")

def test_invalid_icon(self):
"""Test that an error is raised if an invalid icon is provided."""
with self.assertRaises(StreamlitAPIException) as e:
st.link_button("the label", url="https://streamlit.io", icon="invalid")
self.assertEqual(
str(e.exception),
'The value "invalid" is not a valid emoji. Shortcodes are not allowed, please use a single character instead.',
)
1 change: 1 addition & 0 deletions proto/streamlit/proto/LinkButton.proto
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ message LinkButton {
bool disabled = 7;
bool use_container_width = 8;
string type = 9;
string icon = 10;
}

0 comments on commit c94837e

Please sign in to comment.