Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add vertical alignment parameter to st.columns #8568

Merged
merged 31 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
1a55246
Implement alignment
LukasMasuch Apr 26, 2024
d77ee13
Add extra margin for checkbox
LukasMasuch Apr 27, 2024
8a961f5
Merge remote-tracking branch 'upstream/develop' into feature/vertical…
LukasMasuch May 25, 2024
7bfce89
Update parameter name
LukasMasuch May 27, 2024
1d03b0b
Add docstring
LukasMasuch May 27, 2024
081a27d
Merge remote-tracking branch 'upstream/develop' into feature/vertical…
LukasMasuch May 28, 2024
d0e3189
Rename property
LukasMasuch May 28, 2024
545779d
Add unit tests
LukasMasuch May 28, 2024
3f24902
Move layouts tests to `elements` folder
LukasMasuch May 28, 2024
f2dcaa7
Minor fix
LukasMasuch May 28, 2024
f3ba8df
Fix type
LukasMasuch May 28, 2024
e50e04a
Fix tests
LukasMasuch May 28, 2024
cbc191b
Fix name
LukasMasuch May 28, 2024
7566b97
Use literal for gap
LukasMasuch May 28, 2024
dc1d3bb
Merge remote-tracking branch 'upstream/develop' into feature/vertical…
LukasMasuch May 29, 2024
009eb11
Add e2e tests
LukasMasuch May 29, 2024
b2fac32
Update snapshots
LukasMasuch May 29, 2024
6c0b962
Update comments
LukasMasuch May 29, 2024
4fe0da8
Remove unused st_columns script
LukasMasuch May 30, 2024
fa35199
Merge branch 'develop' into feature/vertical_alignment
LukasMasuch May 30, 2024
4fffd1c
Clean up mapping
LukasMasuch Jun 3, 2024
285b1ec
Merge remote-tracking branch 'upstream/develop' into feature/vertical…
LukasMasuch Jun 3, 2024
1b2e6e2
Recreate vertical alignment snapshots
LukasMasuch Jun 3, 2024
8b0b13b
Use styled component instead of class name
LukasMasuch Jun 3, 2024
4be965d
Update snapshot
LukasMasuch Jun 3, 2024
273d01c
Improve styled component
LukasMasuch Jun 3, 2024
154f638
Merge branch 'develop' into feature/vertical_alignment
LukasMasuch Jun 4, 2024
2bce740
Merge remote-tracking branch 'upstream/develop' into feature/vertical…
LukasMasuch Jun 11, 2024
b971a4a
Fix comment
LukasMasuch Jun 11, 2024
724d3ff
Merge branch 'develop' into feature/vertical_alignment
LukasMasuch Jun 11, 2024
1a7643e
Merge branch 'develop' into feature/vertical_alignment
LukasMasuch Jun 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions e2e_playwright/st_columns.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,24 @@
subcol1.write(LOREM_IPSUM)
subcol2.write(LOREM_IPSUM)

with st.expander("Vertical alignment - top", expanded=True):
col1, col2, col3 = st.columns(3, vertical_alignment="top")
col1.text_input("Text input (top)")
col2.button("Button (top)", use_container_width=True)
col3.checkbox("Checkbox (top)")

with st.expander("Vertical alignment - center", expanded=True):
col1, col2, col3 = st.columns(3, vertical_alignment="center")
col1.text_input("Text input (center)")
col2.button("Button (center)", use_container_width=True)
col3.checkbox("Checkbox (center)")

with st.expander("Vertical alignment - bottom", expanded=True):
col1, col2, col3 = st.columns(3, vertical_alignment="bottom")
col1.text_input("Text input (bottom)")
col2.button("Button (bottom)", use_container_width=True)
col3.checkbox("Checkbox (bottom)")

if st.button("Nested columns - two levels (raises exception)"):
col1, col2 = st.columns(2)
with col1:
Expand Down
45 changes: 45 additions & 0 deletions e2e_playwright/st_columns_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
# We use regex here since some browsers may resolve this to two numbers:
expect(column_gap_small).to_have_css("gap", re.compile("16px"))
column_gap_small.scroll_into_view_if_needed()
assert_snapshot(

Check failure on line 72 in e2e_playwright/st_columns_test.py

View workflow job for this annotation

GitHub Actions / playwright-e2e-tests

test_column_gap_small_is_correctly_applied[webkit] Failed: Snapshot mismatch for st_columns-column_gap_small[webkit] (2336 pixels difference; 2.46%)
column_gap_small,
name="st_columns-column_gap_small",
image_threshold=CAT_IMAGE_THRESHOLD,
Expand Down Expand Up @@ -156,6 +156,51 @@
)


def test_column_vertical_alignment_top(
app: Page, assert_snapshot: ImageCompareFunction
):
"""Test that vertical alignment top works correctly."""
column_gap_small = (
get_expander(app, "Vertical alignment - top")
.get_by_test_id("stHorizontalBlock")
.nth(0)
)
assert_snapshot(
column_gap_small,
name="st_columns-vertical_alignment_top",
)


def test_column_vertical_alignment_center(
app: Page, assert_snapshot: ImageCompareFunction
):
"""Test that vertical alignment center works correctly."""
column_gap_small = (
get_expander(app, "Vertical alignment - center")
.get_by_test_id("stHorizontalBlock")
.nth(0)
)
assert_snapshot(
column_gap_small,
name="st_columns-vertical_alignment_center",
)


def test_column_vertical_alignment_bottom(
app: Page, assert_snapshot: ImageCompareFunction
):
"""Test that vertical alignment center works correctly."""
column_gap_small = (
get_expander(app, "Vertical alignment - bottom")
.get_by_test_id("stHorizontalBlock")
.nth(0)
)
assert_snapshot(
column_gap_small,
name="st_columns-vertical_alignment_bottom",
)


def test_two_level_nested_columns_shows_exception(app: Page):
"""Shows exception when trying to nest columns more than one level deep."""

Expand Down
3 changes: 3 additions & 0 deletions frontend/lib/src/components/core/Block/Block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ const BlockNodeRenderer = (props: BlockPropsWithWidth): ReactElement => {
<StyledColumn
weight={node.deltaBlock.column.weight ?? 0}
gap={node.deltaBlock.column.gap ?? ""}
verticalAlignment={
node.deltaBlock.column.verticalAlignment ?? undefined
}
data-testid="column"
>
{child}
Expand Down
20 changes: 19 additions & 1 deletion frontend/lib/src/components/core/Block/styled-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@

import React from "react"
import styled from "@emotion/styled"

import { EmotionTheme } from "@streamlit/lib/src/theme"
import { StyledCheckbox } from "@streamlit/lib/src/components/widgets/Checkbox/styled-components"
import { Block as BlockProto } from "@streamlit/lib/src/proto"

function translateGapWidth(gap: string, theme: EmotionTheme): string {
let gapWidth = theme.spacing.lg
Expand Down Expand Up @@ -113,10 +116,12 @@ export const StyledElementContainer = styled.div<StyledElementContainerProps>(
interface StyledColumnProps {
weight: number
gap: string
verticalAlignment?: BlockProto.Column.VerticalAlignment
}

export const StyledColumn = styled.div<StyledColumnProps>(
({ weight, gap, theme }) => {
({ weight, gap, theme, verticalAlignment }) => {
const { VerticalAlignment } = BlockProto.Column
const percentage = weight * 100
const gapWidth = translateGapWidth(gap, theme)
const width = `calc(${percentage}% - ${gapWidth})`
Expand All @@ -130,6 +135,19 @@ export const StyledColumn = styled.div<StyledColumnProps>(
[`@media (max-width: ${theme.breakpoints.columns})`]: {
minWidth: `calc(100% - ${theme.spacing.twoXL})`,
},
...(verticalAlignment === VerticalAlignment.BOTTOM && {
marginTop: "auto",
// Add margin to the last checkbox within the column to align it
// better with other input widgets. This is a temporary fix
// until we have a better solution for this.
[`& ${StyledElementContainer}:last-of-type > ${StyledCheckbox}`]: {
marginBottom: theme.spacing.sm,
},
}),
...(verticalAlignment === VerticalAlignment.CENTER && {
marginTop: "auto",
marginBottom: "auto",
}),
}
}
)
Expand Down
9 changes: 4 additions & 5 deletions frontend/lib/src/components/widgets/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { Placement } from "@streamlit/lib/src/components/shared/Tooltip"
import { StyledWidgetLabelHelpInline } from "@streamlit/lib/src/components/widgets/BaseWidget"
import StreamlitMarkdown from "@streamlit/lib/src/components/shared/StreamlitMarkdown"

import { StyledContent } from "./styled-components"
import { StyledContent, StyledCheckbox } from "./styled-components"

export interface OwnProps {
disabled: boolean
Expand Down Expand Up @@ -136,7 +136,6 @@ class Checkbox extends React.PureComponent<Props, State> {
const { colors, spacing, sizes } = theme
const lightTheme = hasLightBackgroundColor(theme)

const style = { width }
const color = disabled ? colors.fadedText40 : colors.bodyText

// Manage our form-clear event handler.
Expand All @@ -147,10 +146,10 @@ class Checkbox extends React.PureComponent<Props, State> {
)

return (
<div
<StyledCheckbox
className="row-widget stCheckbox"
data-testid="stCheckbox"
style={style}
width={width}
>
<UICheckbox
checked={this.state.value}
Expand Down Expand Up @@ -294,7 +293,7 @@ class Checkbox extends React.PureComponent<Props, State> {
)}
</StyledContent>
</UICheckbox>
</div>
</StyledCheckbox>
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ import styled from "@emotion/styled"

import { LabelVisibilityOptions } from "@streamlit/lib/src/util/utils"

export interface StyledCheckboxProps {
width: number
}

export const StyledCheckbox = styled.div<StyledCheckboxProps>(({ width }) => ({
width,
}))

export interface StyledContentProps {
visibility?: LabelVisibilityOptions
}
Expand Down
41 changes: 32 additions & 9 deletions lib/streamlit/elements/layouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ def container(
block_proto = BlockProto()
block_proto.allow_empty = False
block_proto.vertical.border = border or False

if height:
# Activate scrolling container behavior:
block_proto.allow_empty = True
Expand All @@ -144,7 +145,11 @@ def container(

@gather_metrics("columns")
def columns(
self, spec: SpecType, *, gap: str | None = "small"
self,
spec: SpecType,
*,
gap: Literal["small", "medium", "large"] = "small",
vertical_alignment: Literal["top", "center", "bottom"] = "top",
) -> list[DeltaGenerator]:
"""Insert containers laid out as side-by-side columns.

Expand Down Expand Up @@ -176,6 +181,9 @@ def columns(
gap : "small", "medium", or "large"
The size of the gap between the columns. Defaults to "small".

vertical_alignment : "top", "center", or "bottom"
The vertical alignment of the content inside the columns. Defaults to "top".

Returns
-------
list of containers
Expand Down Expand Up @@ -225,21 +233,33 @@ def columns(

"""
weights = spec
weights_exception = StreamlitAPIException(
"The input argument to st.columns must be either a "
"positive integer or a list of positive numeric weights. "
"See [documentation](https://docs.streamlit.io/library/api-reference/layout/st.columns) "
"for more information."
)

if isinstance(weights, int):
# If the user provided a single number, expand into equal weights.
# E.g. (1,) * 3 => (1, 1, 1)
# NOTE: A negative/zero spec will expand into an empty tuple.
weights = (1,) * weights

if len(weights) == 0 or any(weight <= 0 for weight in weights):
raise weights_exception
raise StreamlitAPIException(
"The input argument to st.columns must be either a "
"positive integer or a list of positive numeric weights. "
"See [documentation](https://docs.streamlit.io/library/api-reference/layout/st.columns) "
"for more information."
)

vertical_alignment_mapping: dict[
str, BlockProto.Column.VerticalAlignment.ValueType
] = {
"top": BlockProto.Column.VerticalAlignment.TOP,
"center": BlockProto.Column.VerticalAlignment.CENTER,
"bottom": BlockProto.Column.VerticalAlignment.BOTTOM,
}

if vertical_alignment not in vertical_alignment_mapping:
raise StreamlitAPIException(
'The `vertical_alignment` argument to st.columns must be "top", "center", or "bottom". \n'
f"The argument passed was {vertical_alignment}."
)

def column_gap(gap):
if isinstance(gap, str):
Expand All @@ -260,6 +280,9 @@ def column_proto(normalized_weight: float) -> BlockProto:
col_proto = BlockProto()
col_proto.column.weight = normalized_weight
col_proto.column.gap = gap_size
col_proto.column.vertical_alignment = vertical_alignment_mapping[
vertical_alignment
]
col_proto.allow_empty = True
return col_proto

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from parameterized import parameterized

import streamlit as st
from streamlit.errors import StreamlitAPIException
from streamlit.proto.Block_pb2 import Block as BlockProto
from tests.delta_generator_test_case import DeltaGeneratorTestCase


Expand All @@ -33,10 +36,54 @@ def test_equal_width_columns(self):
columns_blocks = all_deltas[1:4]
# 7 elements will be created: 1 horizontal block, 3 columns, 3 markdown
self.assertEqual(len(all_deltas), 7)

# Check the defaults have been applied correctly for the first column
self.assertEqual(
columns_blocks[0].add_block.column.vertical_alignment,
BlockProto.Column.VerticalAlignment.TOP,
)
self.assertEqual(columns_blocks[0].add_block.column.gap, "small")

# Check the weights are correct
self.assertEqual(columns_blocks[0].add_block.column.weight, 1.0 / 3)
self.assertEqual(columns_blocks[1].add_block.column.weight, 1.0 / 3)
self.assertEqual(columns_blocks[2].add_block.column.weight, 1.0 / 3)

@parameterized.expand(
[
("bottom", BlockProto.Column.VerticalAlignment.BOTTOM),
("top", BlockProto.Column.VerticalAlignment.TOP),
("center", BlockProto.Column.VerticalAlignment.CENTER),
]
)
def test_columns_with_vertical_alignment(
self, vertical_alignment: str, expected_alignment
):
"""Test that it works correctly with vertical_alignment argument"""

st.columns(3, vertical_alignment=vertical_alignment)

all_deltas = self.get_all_deltas_from_queue()

# 7 elements will be created: 1 horizontal block, 3 columns, 3 markdown
columns_blocks = all_deltas[1:4]

# Check that the vertical alignment is correct for all columns
assert (
columns_blocks[0].add_block.column.vertical_alignment == expected_alignment
)
assert (
columns_blocks[1].add_block.column.vertical_alignment == expected_alignment
)
assert (
columns_blocks[2].add_block.column.vertical_alignment == expected_alignment
)

def test_columns_with_invalid_vertical_alignment(self):
"""Test that it throws an error on invalid vertical_alignment argument"""
with self.assertRaises(StreamlitAPIException):
st.columns(3, vertical_alignment="invalid")

def test_not_equal_width_int_columns(self):
"""Test that it works correctly when spec is list of ints"""
weights = [3, 2, 1]
Expand Down Expand Up @@ -96,7 +143,7 @@ def test_columns_with_default_small_gap(self):
def test_columns_with_medium_gap(self):
"""Test that it works correctly with "medium" gap argument"""

columns = st.columns(3, gap="medium")
st.columns(3, gap="medium")

all_deltas = self.get_all_deltas_from_queue()

Expand All @@ -113,7 +160,7 @@ def test_columns_with_medium_gap(self):
def test_columns_with_large_gap(self):
"""Test that it works correctly with "large" gap argument"""

columns = st.columns(3, gap="LARGE")
st.columns(3, gap="LARGE")

all_deltas = self.get_all_deltas_from_queue()

Expand Down
7 changes: 7 additions & 0 deletions proto/streamlit/proto/Block.proto
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ message Block {
message Column {
double weight = 1;
string gap = 2;

enum VerticalAlignment {
TOP = 0;
CENTER = 1;
BOTTOM = 2;
}
VerticalAlignment vertical_alignment = 3;
}

message Expandable {
Expand Down