Skip to content

Commit

Permalink
Add icon to st.popover (streamlit#9367)
Browse files Browse the repository at this point in the history
## Describe your changes
Added icon param for `st.popover` -- supports emojis and Material icons

## 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-pchiu authored Aug 30, 2024
1 parent 3ec3d44 commit d1cad8f
Show file tree
Hide file tree
Showing 19 changed files with 105 additions and 3 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.
7 changes: 6 additions & 1 deletion e2e_playwright/st_popover.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
col3.text_input("Column 3")
st.selectbox("Selectbox", ["a", "b", "c"])


with st.popover("popover 4 (with dataframe)", help="help text"):
st.markdown("Popover with dataframe")
st.dataframe(df, use_container_width=False)
Expand All @@ -51,5 +50,11 @@
with st.popover("popover 6 (disabled)", disabled=True):
st.markdown("Hello World 👋")

with st.popover("popover 7 (emoji)", icon="🦄"):
st.markdown("Hello unicorn")

with st.popover("popover 8 (material icon)", icon=":material/thumb_up:"):
st.markdown("Hello thumb up")

with st.expander("Output"):
st.markdown(text)
4 changes: 3 additions & 1 deletion e2e_playwright/st_popover_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,16 @@ def test_popover_button_rendering(
):
"""Test that the popover buttons are correctly rendered via screenshot matching."""
popover_elements = themed_app.get_by_test_id("stPopover")
expect(popover_elements).to_have_count(6)
expect(popover_elements).to_have_count(8)

assert_snapshot(popover_elements.nth(0), name="st_popover-sidebar")
assert_snapshot(popover_elements.nth(1), name="st_popover-empty")
assert_snapshot(popover_elements.nth(2), name="st_popover-use_container_width")
assert_snapshot(popover_elements.nth(3), name="st_popover-normal")
# Popover button 4 is almost the same as 3, so we don't need to test it
assert_snapshot(popover_elements.nth(5), name="st_popover-disabled")
assert_snapshot(popover_elements.nth(6), name="st_popover-emoji_icon")
assert_snapshot(popover_elements.nth(7), name="st_popover-material_icon")


def test_popover_container_rendering(
Expand Down
14 changes: 14 additions & 0 deletions frontend/lib/src/components/elements/Popover/Popover.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,18 @@ describe("Popover container", () => {
// Text should be visible now
expect(screen.queryByText("test")).toBeVisible()
})

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

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

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

const icon = screen.getByTestId("stIconMaterial")
expect(icon).toHaveTextContent("thumb_up")
})
})
16 changes: 15 additions & 1 deletion frontend/lib/src/components/elements/Popover/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ import { ExpandLess, ExpandMore } from "@emotion-icons/material-outlined"
import { PLACEMENT, TRIGGER_TYPE, Popover as UIPopover } from "baseui/popover"

import { hasLightBackgroundColor } from "@streamlit/lib/src/theme"
import { StyledIcon } from "@streamlit/lib/src/components/shared/Icon"
import {
DynamicIcon,
StyledIcon,
} from "@streamlit/lib/src/components/shared/Icon"
import { Block as BlockProto } from "@streamlit/lib/src/proto"
import BaseButton, {
BaseButtonKind,
Expand Down Expand Up @@ -55,6 +58,9 @@ const Popover: React.FC<React.PropsWithChildren<PopoverProps>> = ({
// we need to pass the container width down to the button
const fluidButtonWidth = 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 data-testid="stPopover" className="stPopover">
<UIPopover
Expand Down Expand Up @@ -137,6 +143,14 @@ const Popover: React.FC<React.PropsWithChildren<PopoverProps>> = ({
fluidWidth={element.useContainerWidth ? fluidButtonWidth : false}
onClick={() => setOpen(!open)}
>
{element.icon && (
<DynamicIcon
size={isMaterialIcon ? "lg" : "base"}
margin={isMaterialIcon ? "0 sm 0 0" : "0 md 0 0"}
color={theme.colors.bodyText}
iconValue={element.icon}
/>
)}
<StreamlitMarkdown
source={element.label}
allowHTML={false}
Expand Down
20 changes: 20 additions & 0 deletions lib/streamlit/elements/layouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,7 @@ def popover(
label: str,
*,
help: str | None = None,
icon: str | None = None,
disabled: bool = False,
use_container_width: bool = False,
) -> DeltaGenerator:
Expand Down Expand Up @@ -593,6 +594,23 @@ def popover(
An optional tooltip that gets displayed when the popover button is
hovered over.
icon : str
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 popover button if set to
True. The default is False.
Expand Down Expand Up @@ -653,6 +671,8 @@ def popover(
popover_proto.disabled = disabled
if help:
popover_proto.help = str(help)
if icon is not None:
popover_proto.icon = validate_icon_or_emoji(icon)

block_proto = BlockProto()
block_proto.allow_empty = True
Expand Down
46 changes: 46 additions & 0 deletions lib/tests/streamlit/elements/layouts_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,52 @@ def test_help(self):
self.assertEqual(popover_block.add_block.popover.label, "label")
self.assertEqual(popover_block.add_block.popover.help, "help text")

def test_valid_emoji_icon(self):
"""Test that it can be called with an emoji icon"""
popover = st.popover("label", icon="🦄")

with popover:
# Noop
pass

popover_block = self.get_delta_from_queue()
self.assertEqual(popover_block.add_block.popover.label, "label")
self.assertEqual(popover_block.add_block.popover.icon, "🦄")

def test_valid_material_icon(self):
"""Test that it can be called with a material icon"""
popover = st.popover("label", icon=":material/download:")

with popover:
# Noop
pass

popover_block = self.get_delta_from_queue()
self.assertEqual(popover_block.add_block.popover.label, "label")
self.assertEqual(popover_block.add_block.popover.icon, ":material/download:")

def test_invalid_emoji_icon(self):
"""Test that it throws an error on invalid emoji icon"""
with self.assertRaises(StreamlitAPIException) as e:
st.popover("label", 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.",
)

def test_invalid_material_icon(self):
"""Test that it throws an error on invalid material icon"""
icon = ":material/invalid:"
invisible_white_space = "\u200b"
with self.assertRaises(StreamlitAPIException) as e:
st.popover("label", icon=icon)
self.assertEqual(
str(e.exception),
f'The value `"{icon.replace("/", invisible_white_space + "/")}"` is not a valid Material icon.'
f" Please use a Material icon shortcode like **`:material{invisible_white_space}/thumb_up:`**. ",
)


class StatusContainerTest(DeltaGeneratorTestCase):
def test_label_required(self):
Expand Down
1 change: 1 addition & 0 deletions proto/streamlit/proto/Block.proto
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ message Block {
bool use_container_width = 2;
string help = 3;
bool disabled = 4;
string icon = 5;
}


Expand Down

0 comments on commit d1cad8f

Please sign in to comment.