Skip to content

Commit

Permalink
st.radio - markdown enabled captions (streamlit#7105)
Browse files Browse the repository at this point in the history
st.radio - adding a keyword-only param that allows you to add captions/descriptions to radio button options.
These captions support the same markdown as radio button labels (which also don't allow links).
  • Loading branch information
mayagbarnes authored and zyxue committed Apr 16, 2024
1 parent 7eac421 commit 30bc66a
Show file tree
Hide file tree
Showing 23 changed files with 200 additions and 38 deletions.
19 changes: 17 additions & 2 deletions e2e/scripts/st_radio.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,28 @@
i9 = st.radio("radio 9", markdown_options)
st.write("value 9:", i9)

i10 = st.radio(
"radio 10 - captions",
["A", "B", "C", "D", "E", "F", "G"],
captions=markdown_options,
)
st.write("value 10:", i10)

i11 = st.radio(
"radio 11 - horizontal, captions",
["yes", "maybe", "no"],
captions=["Opt in", "", "Opt out"],
horizontal=True,
)
st.write("value 11:", i11)

if runtime.exists():

def on_change():
st.session_state.radio_changed = True

st.radio("radio 10", options, 1, key="radio10", on_change=on_change)
st.write("value 10:", st.session_state.radio10)
st.radio("radio 12", options, 1, key="radio10", on_change=on_change)
st.write("value 12:", st.session_state.radio10)
st.write("radio changed:", "radio_changed" in st.session_state)

st.radio("PySpark radio", pyspark_mocks.DataFrame()) # type: ignore
33 changes: 28 additions & 5 deletions e2e/specs/st_radio.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe("st.radio", () => {
});

it("shows widget correctly", () => {
cy.get(".stRadio").should("have.length", 11);
cy.get(".stRadio").should("have.length", 13);

cy.get(".stRadio").each((el, idx) => {
return cy.wrap(el).matchThemedSnapshots("radio" + idx);
Expand Down Expand Up @@ -96,11 +96,30 @@ describe("st.radio", () => {
"value 7: female" +
"value 8: female" +
"value 9: bold text" +
"value 10: male" +
"value 10: A" +
"value 11: yes" +
"value 12: male" +
"radio changed: False"
);
});

it("has correct caption values", () => {
const captions = [ "bold text",
"italics text",
"strikethrough text",
"shortcode: 😊",
"link text",
"code text",
"red blue green violet orange",
"Opt in",
"\u00a0",
"Opt out",
]
captions.forEach((expected, idx) => {
cy.getIndexed("[data-testid='stCaptionContainer']", idx).should('contain.text', expected)
})
});

it("formats display values", () => {
cy.getIndexed('.stRadio [role="radiogroup"]', 1).should(
"have.text",
Expand Down Expand Up @@ -141,13 +160,15 @@ describe("st.radio", () => {
"value 7: male" +
"value 8: male" +
"value 9: red blue green violet orange" +
"value 10: male" +
"value 10: G" +
"value 11: no" +
"value 12: male" +
"radio changed: False"
);
});

it("calls callback if one is registered", () => {
cy.getIndexed(".stRadio", 9).then(el => {
cy.getIndexed(".stRadio", 11).then(el => {
return cy
.wrap(el)
.find("input")
Expand All @@ -166,7 +187,9 @@ describe("st.radio", () => {
"value 7: female" +
"value 8: female" +
"value 9: bold text" +
"value 10: female" +
"value 10: A" +
"value 11: yes" +
"value 12: female" +
"radio changed: True"
);
});
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 29 additions & 0 deletions frontend/lib/src/components/shared/Radio/Radio.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const getProps = (props: Partial<Props> = {}): Props => ({
value: 0,
onChange: () => {},
options: ["a", "b", "c"],
captions: [],
label: "Label",
theme: mockTheme.emotion,
...props,
Expand Down Expand Up @@ -123,6 +124,34 @@ describe("Radio widget", () => {
})
})

it("doesn't render captions when there are none", () => {
const props = getProps()
render(<Radio {...props} />)

expect(screen.queryAllByTestId("stCaptionContainer")).toHaveLength(0)
})

it("renders non-blank captions", () => {
const props = getProps({ captions: ["caption1", "", "caption2"] })
render(<Radio {...props} />)

expect(screen.getAllByTestId("stCaptionContainer")).toHaveLength(3)

expect(screen.getByText("caption1")).toBeInTheDocument()
expect(screen.getByText("caption2")).toBeInTheDocument()
})

it("has the correct captions", () => {
const props = getProps({ captions: ["caption1", "caption2", "caption3"] })
render(<Radio {...props} />)

expect(screen.getAllByTestId("stCaptionContainer")).toHaveLength(3)

props.captions.forEach(caption => {
expect(screen.getByText(caption)).toBeInTheDocument()
})
})

it("shows a message when there are no options to be shown", () => {
const props = getProps({ options: [] })
render(<Radio {...props} />)
Expand Down
30 changes: 28 additions & 2 deletions frontend/lib/src/components/shared/Radio/Radio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface Props {
value: number
onChange: (selectedIndex: number) => any
options: any[]
captions: any[]
label?: string
labelVisibility?: LabelVisibilityOptions
help?: string
Expand Down Expand Up @@ -79,6 +80,15 @@ class Radio extends React.PureComponent<Props, State> {
const { colors, radii } = theme
const style = { width }
const options = [...this.props.options]
const captions = [...this.props.captions]
const hasCaptions = captions.length > 0

const spacerNeeded = (caption: string): string => {
// When captions are provided for only some options in horizontal
// layout we need to add a spacer for the options without captions
const spacer = caption == "" && horizontal && hasCaptions
return spacer ? "&nbsp;" : caption
}

if (options.length === 0) {
options.push("No options to select.")
Expand All @@ -105,6 +115,13 @@ class Radio extends React.PureComponent<Props, State> {
align={horizontal ? ALIGN.horizontal : ALIGN.vertical}
aria-label={label}
data-testid="stRadioGroup"
overrides={{
RadioGroupRoot: {
style: {
gap: hasCaptions ? "0.5rem" : "0",
},
},
}}
>
{options.map((option: string, index: number) => (
<UIRadio
Expand All @@ -119,7 +136,7 @@ class Radio extends React.PureComponent<Props, State> {
}) => ({
marginBottom: 0,
marginTop: 0,
marginRight: "1rem",
marginRight: hasCaptions ? "0.5rem" : "1rem",
// Make left and right padding look the same visually.
paddingLeft: 0,
alignItems: "start",
Expand Down Expand Up @@ -164,8 +181,17 @@ class Radio extends React.PureComponent<Props, State> {
source={option}
allowHTML={false}
isLabel
isButton
largerLabel
disableLinks
/>
{hasCaptions && (
<StreamlitMarkdown
source={spacerNeeded(captions[index])}
allowHTML={false}
isCaption
isLabel
/>
)}
</UIRadio>
))}
</RadioGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,11 +243,16 @@ describe("StreamlitMarkdown", () => {
expect(image).not.toBeInTheDocument()
})

it("doesn't render links when isButton is true", () => {
it("doesn't render links when disableLinks is true", () => {
// Valid markdown further restricted with buttons to eliminate links
const source = "[Link text](www.example.com)"
render(
<StreamlitMarkdown source={source} allowHTML={false} isLabel isButton />
<StreamlitMarkdown
source={source}
allowHTML={false}
isLabel
disableLinks
/>
)
const tag = screen.getByText("Link text")
expect(tag instanceof HTMLAnchorElement).toBe(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ export interface Props {
largerLabel?: boolean

/**
* Does not allow links & has larger font sizing
* Does not allow links
*/
isButton?: boolean
disableLinks?: boolean

/**
* Toast has smaller font sizing
Expand Down Expand Up @@ -227,9 +227,9 @@ export interface RenderedMarkdownProps {
isLabel?: boolean

/**
* Does not allow links & has larger font sizing
* Does not allow links
*/
isButton?: boolean
disableLinks?: boolean
}

export type CustomCodeTagProps = JSX.IntrinsicElements["code"] &
Expand Down Expand Up @@ -264,7 +264,7 @@ export function RenderedMarkdown({
source,
overrideComponents,
isLabel,
isButton,
disableLinks,
}: RenderedMarkdownProps): ReactElement {
const renderers: Components = {
pre: CodeBlock,
Expand Down Expand Up @@ -343,8 +343,8 @@ export function RenderedMarkdown({
"input",
"hr",
"blockquote",
// Button labels additionally restrict links
...(isButton ? ["a"] : []),
// additionally restrict links
...(disableLinks ? ["a"] : []),
]

return (
Expand Down Expand Up @@ -389,7 +389,7 @@ class StreamlitMarkdown extends PureComponent<Props> {
isCaption,
isLabel,
largerLabel,
isButton,
disableLinks,
isToast,
} = this.props
const isInSidebar = this.context
Expand All @@ -400,7 +400,6 @@ class StreamlitMarkdown extends PureComponent<Props> {
isInSidebar={isInSidebar}
isLabel={isLabel}
largerLabel={largerLabel}
isButton={isButton}
isToast={isToast}
style={style}
data-testid={isCaption ? "stCaptionContainer" : "stMarkdownContainer"}
Expand All @@ -409,7 +408,7 @@ class StreamlitMarkdown extends PureComponent<Props> {
source={source}
allowHTML={allowHTML}
isLabel={isLabel}
isButton={isButton}
disableLinks={disableLinks}
/>
</StyledStreamlitMarkdown>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export interface StyledStreamlitMarkdownProps {
isInSidebar: boolean
isLabel?: boolean
largerLabel?: boolean
isButton?: boolean
isToast?: boolean
}

Expand All @@ -40,18 +39,10 @@ function sharedMarkdownStyle(theme: Theme): any {

export const StyledStreamlitMarkdown =
styled.div<StyledStreamlitMarkdownProps>(
({
theme,
isCaption,
isInSidebar,
isLabel,
largerLabel,
isButton,
isToast,
}) => {
({ theme, isCaption, isInSidebar, isLabel, largerLabel, isToast }) => {
// Widget Labels have smaller font size with exception of Button/Checkbox/Radio Button labels
// Toasts also have smaller font size
const labelFontSize = (isLabel && !largerLabel && !isButton) || isToast
const labelFontSize = (isLabel && !largerLabel) || isToast
return {
fontFamily: theme.genericFonts.bodyFont,
marginBottom: isLabel ? "" : `-${theme.spacing.lg}`,
Expand Down
3 changes: 2 additions & 1 deletion frontend/lib/src/components/widgets/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ function Button(props: Props): ReactElement {
source={element.label}
allowHTML={false}
isLabel
isButton
largerLabel
disableLinks
/>
</BaseButton>
</BaseButtonTooltip>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ function DownloadButton(props: Props): ReactElement {
source={element.label}
allowHTML={false}
isLabel
isButton
largerLabel
disableLinks
/>
</BaseButton>
</BaseButtonTooltip>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ export function FormSubmitButton(props: Props): ReactElement {
source={element.label}
allowHTML={false}
isLabel
isButton
largerLabel
disableLinks
/>
</BaseButton>
</BaseButtonTooltip>
Expand Down
26 changes: 26 additions & 0 deletions frontend/lib/src/components/widgets/Radio/Radio.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const getProps = (
label: "Label",
default: 0,
options: ["a", "b", "c"],
captions: [],
...elementProps,
}),
width: 0,
Expand Down Expand Up @@ -113,6 +114,31 @@ describe("Radio widget", () => {
})
})

it("renders no captions when none passed", () => {
const props = getProps()
render(<Radio {...props} />)

expect(screen.queryAllByTestId("stCaptionContainer")).toHaveLength(0)
})

it("has the correct captions", () => {
const props = getProps({ captions: ["caption1", "caption2", "caption3"] })
render(<Radio {...props} />)

expect(screen.getAllByTestId("stCaptionContainer")).toHaveLength(3)
props.element.options.forEach(option => {
expect(screen.getByText(option)).toBeInTheDocument()
})
})

it("renders non-blank captions", () => {
const props = getProps({ captions: ["caption1", "", ""] })
render(<Radio {...props} />)

expect(screen.getAllByTestId("stCaptionContainer")).toHaveLength(3)
expect(screen.getByText("caption1")).toBeInTheDocument()
})

it("shows a message when there are no options to be shown", () => {
const props = getProps({ options: [] })
render(<Radio {...props} />)
Expand Down

0 comments on commit 30bc66a

Please sign in to comment.