Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c9a4b96
Add title parameter to alert elements
lukasmasuch Apr 7, 2026
d3c5c21
fix: address CI failures and PR review comments
lukasmasuch Apr 7, 2026
fddb22d
[snapshots] Update E2E snapshots for #14665 (#14666)
github-actions[bot] Apr 7, 2026
42d55cd
Support Markdown in alert title parameter
lukasmasuch Apr 7, 2026
7525fc5
Fix alert title + icon layout to match mockup
lukasmasuch Apr 7, 2026
886076d
Use styled.strong for StyledAlertTitle for accessibility
lukasmasuch Apr 7, 2026
033885a
[snapshots] Update E2E snapshots for #14665 (#14667)
github-actions[bot] Apr 7, 2026
f5b6b31
Fix alert title bold styling and improve docstrings
lukasmasuch Apr 8, 2026
5783731
Fix alert styling and clean up title examples
lukasmasuch Apr 8, 2026
266bbf6
Fix E2E test element count mismatch for alert title tests
lukasmasuch Apr 8, 2026
79b567f
[snapshots] Update E2E snapshots for #14665 (#14684)
github-actions[bot] Apr 8, 2026
0397115
Fix code block overflow in alerts with icons
lukasmasuch Apr 8, 2026
53f0369
Fix code block overflow by passing style directly to StreamlitMarkdown
lukasmasuch Apr 8, 2026
b3da72a
Simplify alert title E2E tests
lukasmasuch Apr 9, 2026
a60b09f
[snapshots] Update E2E snapshots for #14665 (#14703)
github-actions[bot] Apr 9, 2026
14206f8
Remove orphaned alert title E2E snapshots
lukasmasuch Apr 9, 2026
d857feb
Delete error_long_code snapshots for regeneration
lukasmasuch Apr 9, 2026
c76e90c
Delete info_with_title_and_icon snapshots for regeneration
lukasmasuch Apr 9, 2026
e2eb239
Delete info_with_title snapshots for regeneration
lukasmasuch Apr 9, 2026
31c9545
Add regenerated alert E2E snapshots
lukasmasuch Apr 9, 2026
cea0d6f
Add celebration icon to success title in E2E test
lukasmasuch Apr 9, 2026
e0585ba
Move celebration icon to end of success title
lukasmasuch Apr 9, 2026
8b3532c
Fix alert title E2E test by using plain text title
lukasmasuch Apr 9, 2026
d7d5eb4
[snapshots] Update E2E snapshots for #14665 (#14727)
github-actions[bot] Apr 11, 2026
adb1973
Use markdown title with icon and substring matching in E2E test
lukasmasuch Apr 11, 2026
5f4f970
[snapshots] Update E2E snapshots for #14665 (#14729)
github-actions[bot] Apr 11, 2026
bb92df4
Address PR review comments for alert title
lukasmasuch Apr 14, 2026
c94bf7c
refactor: consolidate AlertElement return paths and fix title width
lukasmasuch Apr 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
11 changes: 11 additions & 0 deletions e2e_playwright/st_alert.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,14 @@

# Test case with material icon extracted from body
st.info(":material/lightbulb: This info has a material icon extracted from body")

### Test cases for title parameter ###

# Alerts with title
st.success(
"This is the success body text.",
title="Success _Title_ :material/celebration:",
)

# Alert with title and icon
st.info("Body with title and icon.", title="Notice", icon=":material/info:")
42 changes: 39 additions & 3 deletions e2e_playwright/st_alert_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def test_alerts_rendering_themed(
):
"""Test that alerts render correctly with theme-dependent styling."""
alert_elements = themed_app.get_by_test_id("stAlert")
expect(alert_elements).to_have_count(34)
expect(alert_elements).to_have_count(36)

# The first 4 alerts are super basic, no need to screenshot test those
expect(alert_elements.nth(0)).to_have_text("This is an error")
Expand Down Expand Up @@ -71,7 +71,7 @@ def test_alerts_rendering_themed(
def test_alerts_rendering_layout(app: Page, assert_snapshot: ImageCompareFunction):
"""Test that alerts layout variations render correctly (theme-independent)."""
alert_elements = app.get_by_test_id("stAlert")
expect(alert_elements).to_have_count(34)
expect(alert_elements).to_have_count(36)

# Line wrapping (layout behavior)
assert_snapshot(alert_elements.nth(8), name="st_alert-error_line_wrapping_1")
Expand Down Expand Up @@ -108,7 +108,7 @@ def test_material_symbol_from_latest_font_version_rendering(
):
"""Test that icon from latest version material symbols font renders correctly."""
alert_elements = app.get_by_test_id("stAlert")
expect(alert_elements).to_have_count(34)
expect(alert_elements).to_have_count(36)

assert_snapshot(
alert_elements.nth(21),
Expand All @@ -117,3 +117,39 @@ def test_material_symbol_from_latest_font_version_rendering(
image_threshold=0.001,
pixel_threshold=0.025,
)


def test_alert_title_rendering(themed_app: Page, assert_snapshot: ImageCompareFunction):
"""Test that alerts with title parameter render correctly."""
alert_elements = themed_app.get_by_test_id("stAlert")
expect(alert_elements).to_have_count(36)

# Locate title alerts by their stable title text instead of fixture order.
success_with_title = alert_elements.filter(
has=themed_app.get_by_test_id("stAlertTitle").get_by_text("Success")
)
expect(success_with_title).to_have_count(1)
expect(success_with_title.get_by_test_id("stAlertTitle")).to_contain_text("Success")
assert_snapshot(success_with_title, name="st_alert-success_with_title")

# Alert with title and icon
info_with_title_and_icon = alert_elements.filter(
has=themed_app.get_by_test_id("stAlertTitle").get_by_text("Notice", exact=True)
)
expect(info_with_title_and_icon).to_have_count(1)
expect(info_with_title_and_icon.get_by_test_id("stAlertTitle")).to_have_text(
"Notice"
)
expect(
info_with_title_and_icon.get_by_test_id("stAlertDynamicIcon")
).to_be_visible()
assert_snapshot(info_with_title_and_icon, name="st_alert-info_with_title_and_icon")


def test_alert_without_title_has_no_title_element(app: Page):
"""Test that alerts without title do not render a title element."""
alert_elements = app.get_by_test_id("stAlert")

# First alert has no title
first_alert = alert_elements.nth(0)
expect(first_alert.get_by_test_id("stAlertTitle")).not_to_be_attached()
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ const RawElementNodeRenderer = (
<AlertElement
icon={alertProto.icon}
body={alertProto.body}
title={alertProto.title}
kind={getAlertElementKind(alertProto.format)}
{...elementProps}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,43 @@ describe("Alert element", () => {
expect(screen.getByTestId("stAlertDynamicIcon")).toHaveTextContent("👉🏻")
expect(screen.getByText("It's dangerous to go alone.")).toBeInTheDocument()
})

it("renders a title when provided", () => {
const props = getProps({
kind: getAlertElementKind(AlertProto.Format.INFO),
body: "This is the body text.",
title: "Important Notice",
})
render(<AlertElement {...props} />)
expect(screen.getByTestId("stAlert")).toBeVisible()
expect(screen.getByTestId("stAlertTitle")).toHaveTextContent(
"Important Notice"
)
expect(screen.getByText("This is the body text.")).toBeVisible()
})

it("does not render title element when title is not provided", () => {
const props = getProps({
kind: getAlertElementKind(AlertProto.Format.INFO),
body: "Just the body.",
})
render(<AlertElement {...props} />)
expect(screen.getByTestId("stAlert")).toBeVisible()
expect(screen.queryByTestId("stAlertTitle")).not.toBeInTheDocument()
expect(screen.getByText("Just the body.")).toBeVisible()
})

it("renders title with icon", () => {
const props = getProps({
kind: getAlertElementKind(AlertProto.Format.SUCCESS),
body: "Operation completed successfully.",
title: "Success!",
icon: "✅",
})
render(<AlertElement {...props} />)
expect(screen.getByTestId("stAlert")).toBeVisible()
expect(screen.getByTestId("stAlertTitle")).toHaveTextContent("Success!")
expect(screen.getByTestId("stAlertDynamicIcon")).toHaveTextContent("✅")
expect(screen.getByText("Operation completed successfully.")).toBeVisible()
})
})
72 changes: 51 additions & 21 deletions frontend/lib/src/components/elements/AlertElement/AlertElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,18 @@ import { DynamicIcon } from "~lib/components/shared/Icon/DynamicIcon"
import StreamlitMarkdown from "~lib/components/shared/StreamlitMarkdown/StreamlitMarkdown"
import { useEmotionTheme } from "~lib/hooks/useEmotionTheme"

import { StyledAlertContent, StyledAlertIcon } from "./styled-components"
import {
StyledAlertBody,
StyledAlertContent,
StyledAlertHeader,
StyledAlertIcon,
StyledAlertTitle,
} from "./styled-components"

export interface AlertElementProps {
body: string
icon?: string
title?: string
kind: Kind
}

Expand All @@ -37,35 +44,58 @@ export interface AlertElementProps {
function AlertElement({
icon,
body,
title,
kind,
}: Readonly<AlertElementProps>): ReactElement {
const theme = useEmotionTheme()
const markdownWidth = {
// Fix issue #6394 - Need to account for icon size (iconSizes.lg) + gap when icon present
width: icon
? `calc(100% - (${theme.iconSizes.lg} + ${theme.spacing.sm}))`
: "100%",
}

// Fix issue #6394 - Need to account for icon size (iconSizes.lg) + gap when icon present.
// Only needed for the no-title layout where icon and body are side by side in a flex row.
// In the title layout, StyledAlertTitle already constrains width via flexbox.
const markdownWidth = icon
? { width: `calc(100% - (${theme.iconSizes.lg} + ${theme.spacing.sm}))` }
: undefined

// Extract icon element to avoid duplication between title and no-title layouts.
const iconElement = icon ? (
<StyledAlertIcon>
<DynamicIcon iconValue={icon} size="lg" testid="stAlertDynamicIcon" />
</StyledAlertIcon>
) : null

// When there's a title: icon+title in header row, body below (column layout).
// When no title: icon and body side by side (row layout).
return (
<div className="stAlert" data-testid="stAlert">
<AlertContainer kind={kind}>
<StyledAlertContent>
{icon && (
<StyledAlertIcon>
<DynamicIcon
iconValue={icon}
size="lg"
testid="stAlertDynamicIcon"
<StyledAlertContent $hasTitle={!!title}>
{title ? (
<>
<StyledAlertHeader>
{iconElement}
<StyledAlertTitle data-testid="stAlertTitle">
<StreamlitMarkdown
source={title}
allowHTML={false}
isLabel
largerLabel
/>
</StyledAlertTitle>
</StyledAlertHeader>
<StyledAlertBody>
<StreamlitMarkdown source={body} allowHTML={false} />
</StyledAlertBody>
</>
) : (
<>
{iconElement}
<StreamlitMarkdown
source={body}
allowHTML={false}
style={markdownWidth}
/>
</StyledAlertIcon>
</>
)}

<StreamlitMarkdown
source={body}
allowHTML={false}
style={markdownWidth}
/>
</StyledAlertContent>
</AlertContainer>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,38 @@ import styled from "@emotion/styled"

import { StyledCodeBlock } from "~lib/components/elements/CodeBlock/styled-components"

export const StyledAlertContent = styled.div(({ theme }) => ({
export const StyledAlertContent = styled.div<{ $hasTitle?: boolean }>(
({ theme, $hasTitle }) => ({
display: "flex",
// Vertical layout when there's a title, horizontal when there's not
flexDirection: $hasTitle ? "column" : "row",
gap: $hasTitle ? theme.spacing.twoXS : theme.spacing.sm,
width: "100%",

[`${StyledCodeBlock} code`]: {
paddingRight: theme.spacing.lg,
},
})
)

export const StyledAlertHeader = styled.div(({ theme }) => ({
display: "flex",
alignItems: "flex-start",
gap: theme.spacing.sm,
width: "100%",

[`${StyledCodeBlock} code`]: {
paddingRight: theme.spacing.lg,
},
}))

export const StyledAlertIcon = styled.div(({ theme }) => ({
position: "relative",
top: theme.spacing.threeXS,
flexShrink: 0,
}))

export const StyledAlertTitle = styled.strong(({ theme }) => ({
"& p": {
fontWeight: theme.fontWeights.bold,
},
}))

export const StyledAlertBody = styled.div({
width: "100%",
})
Loading
Loading