Skip to content

Commit

Permalink
Allow setting CSS class on elements via key (streamlit#9295)
Browse files Browse the repository at this point in the history
## Describe your changes

Adds support for setting a CSS class on elements using the `key`
parameter. The key gets prefixed with `st-key-` and slugified to conform
with the class naming rules. This PR also adds support for setting a
`key` on `st.container`, which provides a very flexible way to set CSS
keys for any element.

## GitHub Issue Link (if applicable)

- Closes streamlit#3888
- Closes streamlit#5437

## Testing Plan

- Added 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
lukasmasuch authored Sep 3, 2024
1 parent 3259b2c commit afeb7ba
Show file tree
Hide file tree
Showing 69 changed files with 712 additions and 303 deletions.
23 changes: 23 additions & 0 deletions e2e_playwright/shared/app_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,29 @@ def expect_script_state(
)


def get_element_by_key(locator: Locator | Page, key: str) -> Locator:
"""Get an element with the given user-defined key.
Parameters
----------
locator : Locator
The locator to search for the element.
key : str
The user-defined key of the element
Returns
-------
Locator
The element.
"""
class_name = re.sub(r"[^a-zA-Z0-9_-]", "-", key.strip())
class_name = f"st-key-{class_name}"
return locator.locator(f".{class_name}")


def expand_sidebar(app: Page) -> Locator:
"""Expands the sidebar.
Expand Down
16 changes: 14 additions & 2 deletions e2e_playwright/st_altair_chart_basic_select_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,15 @@
import pytest
from playwright.sync_api import Locator, Page, expect

from e2e_playwright.conftest import ImageCompareFunction, wait_for_app_run
from e2e_playwright.shared.app_utils import click_form_button, expect_prefixed_markdown
from e2e_playwright.conftest import (
ImageCompareFunction,
wait_for_app_run,
)
from e2e_playwright.shared.app_utils import (
click_form_button,
expect_prefixed_markdown,
get_element_by_key,
)


@dataclass
Expand Down Expand Up @@ -356,3 +363,8 @@ def test_selection_state_remains_after_unmounting_snapshot(
name="st_altair_chart-scatter_shift_selection",
image_threshold=0.041,
)


def test_custom_css_class_via_key(app: Page):
"""Test that the element can have a custom css class via the key argument."""
expect(get_element_by_key(app, "scatter_point")).to_be_visible()
6 changes: 6 additions & 0 deletions e2e_playwright/st_button_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
check_top_level_class,
click_button,
click_checkbox,
get_element_by_key,
)


Expand Down Expand Up @@ -114,3 +115,8 @@ def test_show_tooltip_on_hover(app: Page, assert_snapshot: ImageCompareFunction)
def test_check_top_level_class(app: Page):
"""Check that the top level class is correctly set."""
check_top_level_class(app, "stButton")


def test_custom_css_class_via_key(app: Page):
"""Test that the element can have a custom css class via the key argument."""
expect(get_element_by_key(app, "button")).to_be_visible()
2 changes: 1 addition & 1 deletion e2e_playwright/st_camera_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import streamlit as st

x = st.camera_input("Label1", help="help1")
x = st.camera_input("Label1", help="help1", key="camera_input_1")

if x is not None:
st.image(x)
Expand Down
7 changes: 6 additions & 1 deletion e2e_playwright/st_camera_input_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from playwright.sync_api import Page, expect

from e2e_playwright.conftest import ImageCompareFunction
from e2e_playwright.shared.app_utils import check_top_level_class
from e2e_playwright.shared.app_utils import check_top_level_class, get_element_by_key


@pytest.mark.skip_browser("webkit")
Expand Down Expand Up @@ -65,3 +65,8 @@ def test_shows_disabled_widget_correctly(
def test_check_top_level_class(app: Page):
"""Check that the top level class is correctly set."""
check_top_level_class(app, "stCameraInput")


def test_custom_css_class_via_key(app: Page):
"""Test that the element can have a custom css class via the key argument."""
expect(get_element_by_key(app, "camera_input_1")).to_be_visible()
7 changes: 6 additions & 1 deletion e2e_playwright/st_chat_input_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from playwright.sync_api import Page, expect

from e2e_playwright.conftest import ImageCompareFunction, rerun_app, wait_for_app_loaded
from e2e_playwright.shared.app_utils import check_top_level_class
from e2e_playwright.shared.app_utils import check_top_level_class, get_element_by_key


def test_chat_input_rendering(app: Page, assert_snapshot: ImageCompareFunction):
Expand Down Expand Up @@ -198,3 +198,8 @@ def test_calls_callback_on_submit(app: Page):
def test_check_top_level_class(app: Page):
"""Check that the top level class is correctly set."""
check_top_level_class(app, "stChatInput")


def test_custom_css_class_via_key(app: Page):
"""Test that the element can have a custom css class via the key argument."""
expect(get_element_by_key(app, "chat_input_3")).to_be_visible()
6 changes: 6 additions & 0 deletions e2e_playwright/st_checkbox_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from e2e_playwright.shared.app_utils import (
check_top_level_class,
expect_help_tooltip,
get_element_by_key,
get_expander,
)

Expand Down Expand Up @@ -125,3 +126,8 @@ def test_grouped_checkboxes_height(app: Page, assert_snapshot: ImageCompareFunct
def test_check_top_level_class(app: Page):
"""Check that the top level class is correctly set."""
check_top_level_class(app, "stCheckbox")


def test_custom_css_class_via_key(app: Page):
"""Test that the element can have a custom css class via the key argument."""
expect(get_element_by_key(app, "checkbox4")).to_be_visible()
2 changes: 1 addition & 1 deletion e2e_playwright/st_color_picker.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def callback():
st.write("Hello world")


c1 = st.color_picker("Default Color", on_change=callback)
c1 = st.color_picker("Default Color", on_change=callback, key="color_picker_1")
st.write("Color 1", c1)

c2 = st.color_picker("New Color", "#EB144C", help="help string")
Expand Down
6 changes: 6 additions & 0 deletions e2e_playwright/st_color_picker_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
check_top_level_class,
click_form_button,
expect_help_tooltip,
get_element_by_key,
)


Expand Down Expand Up @@ -174,3 +175,8 @@ def test_color_picker_in_fragment(app: Page):
def test_check_top_level_class(app: Page):
"""Check that the top level class is correctly set."""
check_top_level_class(app, "stColorPicker")


def test_custom_css_class_via_key(app: Page):
"""Test that the element can have a custom css class via the key argument."""
expect(get_element_by_key(app, "color_picker_1")).to_be_visible()
2 changes: 1 addition & 1 deletion e2e_playwright/st_components_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@
url = "http://not.a.real.url"
test_component = components.declare_component("test_component", url=url)

test_component()
test_component(key="component_1")
14 changes: 14 additions & 0 deletions e2e_playwright/st_components_v1_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
from playwright.sync_api import Page, expect

from e2e_playwright.conftest import ImageCompareFunction
from e2e_playwright.shared.app_utils import (
check_top_level_class,
get_element_by_key,
)


def test_components_iframe_rendering(
Expand Down Expand Up @@ -74,6 +78,16 @@ def test_declare_component_correctly_sets_attr(app: Page):
)


def test_check_top_level_class(app: Page):
"""Check that the top level class is correctly set."""
check_top_level_class(app, "stCustomComponentV1")


def test_custom_css_class_via_key(app: Page):
"""Test that the element can have a custom css class via the key argument."""
expect(get_element_by_key(app, "component_1")).to_be_visible()


# TODO (willhuang1997): Add tests for handling bytes, JSON, DFs, theme
# TODO (willhuang1997):add tests to ensure the messages actually go to the iframe
# Relevant code is here from the past: https://github.com/streamlit/streamlit/blob/3d0b0603627037255790fe55a483f55fce5eff67/frontend/lib/src/components/widgets/CustomComponent/ComponentInstance.test.tsx#L257
Expand Down
2 changes: 1 addition & 1 deletion e2e_playwright/st_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import streamlit as st

container = st.container()
container = st.container(key="first container")

st.write("Line 1")
container.write("Line 2")
Expand Down
18 changes: 17 additions & 1 deletion e2e_playwright/st_container_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@
from playwright.sync_api import Page, expect

from e2e_playwright.conftest import ImageCompareFunction, wait_for_app_run
from e2e_playwright.shared.app_utils import click_button, click_checkbox, get_checkbox
from e2e_playwright.shared.app_utils import (
check_top_level_class,
click_button,
click_checkbox,
get_checkbox,
get_element_by_key,
)


def test_permits_multiple_out_of_order_elements(app: Page):
Expand Down Expand Up @@ -102,3 +108,13 @@ def test_correctly_handles_first_chat_message(
app.get_by_test_id("stVerticalBlockBorderWrapper").nth(5),
name="st_container-added_chat_message",
)


def test_check_top_level_class(app: Page):
"""Check that the top level class is correctly set."""
check_top_level_class(app, "stVerticalBlock")


def test_custom_css_class_via_key(app: Page):
"""Test that the container can have a custom css class via the key argument."""
expect(get_element_by_key(app, "first container")).to_be_visible()
2 changes: 1 addition & 1 deletion e2e_playwright/st_dataframe_interactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
st.write("Another element")


st.data_editor(random_df, num_rows="dynamic")
st.data_editor(random_df, num_rows="dynamic", key="data_editor")


cell_overlay_test_df = pd.DataFrame(
Expand Down
7 changes: 6 additions & 1 deletion e2e_playwright/st_dataframe_interactions_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from playwright.sync_api import FrameLocator, Locator, Page, Route, expect

from e2e_playwright.conftest import IframedPage, ImageCompareFunction, wait_for_app_run
from e2e_playwright.shared.app_utils import expect_prefixed_markdown
from e2e_playwright.shared.app_utils import expect_prefixed_markdown, get_element_by_key
from e2e_playwright.shared.dataframe_utils import click_on_cell, get_open_cell_overlay

# This test suite covers all interactions of dataframe & data_editor
Expand Down Expand Up @@ -440,6 +440,11 @@ def test_text_cell_editing(themed_app: Page, assert_snapshot: ImageCompareFuncti
)


def test_custom_css_class_via_key(app: Page):
"""Test that the element can have a custom css class via the key argument."""
expect(get_element_by_key(app, "data_editor")).to_be_visible()


# TODO(lukasmasuch): Add additional interactive tests:
# - Copy data to clipboard
# - Paste in data
6 changes: 6 additions & 0 deletions e2e_playwright/st_dataframe_selections_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
COMMAND_KEY,
click_form_button,
expect_prefixed_markdown,
get_element_by_key,
)
from e2e_playwright.shared.dataframe_utils import (
calc_middle_cell_position,
Expand Down Expand Up @@ -424,3 +425,8 @@ def test_that_index_cannot_be_selected(app: Page):
"{'selection': {'rows': [], 'columns': ['col_1']}}",
exact_match=True,
)


def test_custom_css_class_via_key(app: Page):
"""Test that the element can have a custom css class via the key argument."""
expect(get_element_by_key(app, "df_selection")).to_be_visible()
11 changes: 10 additions & 1 deletion e2e_playwright/st_date_input_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
from playwright.sync_api import Page, expect

from e2e_playwright.conftest import ImageCompareFunction, wait_for_app_run
from e2e_playwright.shared.app_utils import check_top_level_class, expect_help_tooltip
from e2e_playwright.shared.app_utils import (
check_top_level_class,
expect_help_tooltip,
get_element_by_key,
)


def test_date_input_rendering(themed_app: Page, assert_snapshot: ImageCompareFunction):
Expand Down Expand Up @@ -312,3 +316,8 @@ def test_range_is_empty_if_calendar_closed_empty(app: Page):
def test_check_top_level_class(app: Page):
"""Check that the top level class is correctly set."""
check_top_level_class(app, "stDateInput")


def test_custom_css_class_via_key(app: Page):
"""Test that the element can have a custom css class via the key argument."""
expect(get_element_by_key(app, "date_input_12")).to_be_visible()
16 changes: 15 additions & 1 deletion e2e_playwright/st_download_button_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
from playwright.sync_api import Page, expect

from e2e_playwright.conftest import ImageCompareFunction
from e2e_playwright.shared.app_utils import click_checkbox
from e2e_playwright.shared.app_utils import (
check_top_level_class,
click_checkbox,
get_element_by_key,
)


def test_download_button_widget_rendering(
Expand Down Expand Up @@ -132,3 +136,13 @@ def test_downloads_txt_file_on_click(app: Page):

assert file_name == "hello.txt"
assert file_text == "Hello world!"


def test_check_top_level_class(app: Page):
"""Check that the top level class is correctly set."""
check_top_level_class(app, "stDownloadButton")


def test_custom_css_class_via_key(app: Page):
"""Test that the element can have a custom css class via the key argument."""
expect(get_element_by_key(app, "download_button")).to_be_visible()
6 changes: 6 additions & 0 deletions e2e_playwright/st_feedback_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
check_top_level_class,
click_button,
click_form_button,
get_element_by_key,
get_markdown,
)

Expand Down Expand Up @@ -131,3 +132,8 @@ def test_feedback_remount_keep_value(app: Page):
def test_check_top_level_class(app: Page):
"""Check that the top level class is correctly set."""
check_top_level_class(app, "stButtonGroup")


def test_custom_css_class_via_key(app: Page):
"""Test that the element can have a custom css class via the key argument."""
expect(get_element_by_key(app, "faces_feedback")).to_be_visible()
7 changes: 6 additions & 1 deletion e2e_playwright/st_file_uploader_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from playwright.sync_api import Page, expect

from e2e_playwright.conftest import ImageCompareFunction, rerun_app, wait_for_app_run
from e2e_playwright.shared.app_utils import check_top_level_class
from e2e_playwright.shared.app_utils import check_top_level_class, get_element_by_key


def test_file_uploader_render_correctly(
Expand Down Expand Up @@ -450,3 +450,8 @@ def test_works_inside_form(app: Page):
def test_check_top_level_class(app: Page):
"""Check that the top level class is correctly set."""
check_top_level_class(app, "stFileUploader")


def test_custom_css_class_via_key(app: Page):
"""Test that the element can have a custom css class via the key argument."""
expect(get_element_by_key(app, "single")).to_be_visible()
6 changes: 6 additions & 0 deletions e2e_playwright/st_multiselect_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
check_top_level_class,
click_checkbox,
expect_help_tooltip,
get_element_by_key,
)


Expand Down Expand Up @@ -240,3 +241,8 @@ def test_multiselect_double_selection(app: Page):
def test_check_top_level_class(app: Page):
"""Check that the top level class is correctly set."""
check_top_level_class(app, "stMultiSelect")


def test_custom_css_class_via_key(app: Page):
"""Test that the element can have a custom css class via the key argument."""
expect(get_element_by_key(app, "multiselect 9")).to_be_visible()
Loading

0 comments on commit afeb7ba

Please sign in to comment.