Skip to content

Commit 0e4154a

Browse files
feat: daterange picker
1 parent 360b496 commit 0e4154a

File tree

7 files changed

+116
-57
lines changed

7 files changed

+116
-57
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Check docs and compoenent examples in [![Streamlit App](https://static.streamlit
3535
+ [x] card
3636
+ [x] avatar
3737
+ [x] date_picker
38-
+ [ ] date_range_picker
38+
+ [x] date_range_picker (date_picker with mode="range")
3939
+ [x] table
4040
+ [x] input
4141
+ [x] slider

pages/DatePicker.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
with open("docs/components/date_picker.md", "r") as f:
77
st.markdown(f.read())
88

9-
dt = ui.date_picker(key="date_picker", label="Date Picker")
9+
dt = ui.date_picker(key="date_picker", mode="single", label="Date Picker")
1010

11-
st.write("Date:", dt)
11+
st.write("Date Value:", dt)
1212

13-
dt2 = ui.date_picker(key="date_picker2", label="Date Picker")
13+
dt2 = ui.date_picker(key="date_picker2", mode="range", label="Date Picker")
14+
15+
st.write("Date Range:", dt2)
1416

1517

1618
st.write(ui.date_picker)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "streamlit-shadcn-ui"
7-
version = "0.1.17"
7+
version = "0.1.18"
88
readme = "README.md"
99
keywords = ["streamlit", "shadcn", "ui", "components"]
1010
description = "Using shadcn components in Streamlit"

streamlit_shadcn_ui/components/packages/frontend/src/components/streamlit/datePicker/datePickerContent.tsx

Lines changed: 74 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,43 +6,96 @@ import {
66
PopoverTrigger,
77
} from "@/components/ui/popover";
88
import { useBodyStyle } from "@/hooks/useBodyStyle";
9+
import { format, parse } from "date-fns";
910
import { forwardRef, useEffect, useState } from "react";
1011
import { Streamlit } from "streamlit-component-lib";
1112

12-
function formatDate(date: Date): string {
13-
const year = date.getFullYear();
14-
const month = (date.getMonth() + 1).toString().padStart(2, '0'); // Months are 0-indexed
15-
const day = date.getDate().toString().padStart(2, '0');
13+
type DateRange = {
14+
from: Date;
15+
to?: Date;
16+
};
1617

17-
return `${year}-${month}-${day}`;
18+
function formatDate(date: Date | DateRange | string | string[]): string | [string, string] {
19+
const formatSingleDate = (date: Date) => {
20+
return format(date, "yyyy-MM-dd");
21+
}
22+
if (date instanceof Date) {
23+
return formatSingleDate(date);
24+
} else if (typeof date === "string") {
25+
const d = parse(date, 'yyyy-MM-dd', new Date());
26+
return formatSingleDate(d);
27+
} else if (Array.isArray(date)) {
28+
return [formatDate(date[0]), formatDate(date[1])] as [string, string];
29+
} else {
30+
return [formatDate(date.from), formatDate(date.to)] as [string, string];
31+
}
1832
}
1933

20-
interface StDatePickerContentProps {
21-
value?: string;
34+
type StDatePickerContentProps =
35+
| {
36+
value?: string;
37+
mode: "single";
38+
}
39+
| {
40+
value?: [string, string];
41+
mode: "range";
42+
};
43+
44+
function initDateValue(props: StDatePickerContentProps): Date | DateRange {
45+
const { value, mode } = props;
46+
if (mode === "single") {
47+
return parse(value, 'yyyy-MM-dd', new Date());
48+
} else {
49+
if (!value) {
50+
return {
51+
from: new Date(),
52+
};
53+
}
54+
return {
55+
from: parse(value[0], 'yyyy-MM-dd', new Date()),
56+
to: parse(value[1], 'yyyy-MM-dd', new Date()),
57+
};
58+
}
2259
}
60+
2361
export const StDatePickerContent = forwardRef<
2462
HTMLDivElement,
2563
StDatePickerContentProps
2664
>((props, ref) => {
27-
const { value } = props;
28-
const [date, setDate] = useState<Date>(new Date(value));
65+
const { value, mode } = props;
66+
const [date, setDate] = useState<Date | DateRange>(initDateValue(props));
2967

3068
useEffect(() => {
31-
setDate(new Date(value));
32-
}, [value]);
69+
setDate(
70+
initDateValue({
71+
value,
72+
mode,
73+
} as StDatePickerContentProps)
74+
);
75+
}, [value, mode]);
3376

34-
useBodyStyle("body { background-color: transparent !important; }")
77+
useBodyStyle("body, document { background-color: transparent !important; }");
3578

3679
return (
3780
<Popover open={true}>
3881
<PopoverTrigger className="hidden"></PopoverTrigger>
3982
<PopoverContent ref={ref} className="w-auto p-0">
40-
<Calendar
41-
mode="single"
42-
selected={date}
43-
onSelect={setDate}
44-
initialFocus
45-
/>
83+
{
84+
mode === 'single' && <Calendar
85+
mode="single"
86+
selected={date as Date}
87+
onSelect={setDate as (date: Date) => void}
88+
initialFocus
89+
/>
90+
}
91+
{
92+
mode === 'range' && <Calendar
93+
mode="range"
94+
selected={date as DateRange}
95+
onSelect={setDate as (date: DateRange) => void}
96+
initialFocus
97+
/>
98+
}
4699
<div className="mb-4 mx-4 flex gap-2">
47100
<Button
48101
onClick={() => {
@@ -58,7 +111,9 @@ export const StDatePickerContent = forwardRef<
58111
variant="secondary"
59112
onClick={() => {
60113
Streamlit.setComponentValue({
61-
value: value ? formatDate(new Date(value)) : value,
114+
value: value
115+
? formatDate(value)
116+
: value,
62117
open: false,
63118
});
64119
}}
Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
1-
import { format } from "date-fns";
1+
import { format, parse } from "date-fns";
22
import { Calendar as CalendarIcon } from "lucide-react";
33

44
import { cn, getPositionRelativeToTopDocument } from "@/lib/utils";
55
import { Button } from "@/components/ui/button";
6-
import { Popover, PopoverTrigger } from "@/components/ui/popover";
76
import { forwardRef, useEffect, useState } from "react";
87
import { Streamlit } from "streamlit-component-lib";
98
import { useBodyStyle } from "@/hooks/useBodyStyle";
109
import { Label } from "@/components/ui/label";
1110

1211
interface StDatePickerTriggerProps {
13-
value?: string;
12+
value?: string | string[];
1413
open: boolean;
1514
label?: string;
1615
}
@@ -19,17 +18,21 @@ export const StDatePickerTrigger = forwardRef<
1918
HTMLDivElement,
2019
StDatePickerTriggerProps
2120
>((props, ref) => {
22-
const { label } = props;
21+
const { label, value } = props;
2322
const [open, setOpen] = useState(Boolean(props.open));
2423

25-
const date = props.value ? new Date(props.value) : null;
24+
const date: Date[] = value
25+
? value instanceof Array
26+
? value.map((v) => parse(v, 'yyyy-MM-dd', new Date()))
27+
: [parse(value, 'yyyy-MM-dd', new Date())]
28+
: null;
2629

2730
useEffect(() => {
2831
setOpen(Boolean(props.open));
2932
}, [props.open]);
3033

3134
useEffect(() => {
32-
if (ref && typeof ref !== 'function') {
35+
if (ref && typeof ref !== "function") {
3336
const pos = getPositionRelativeToTopDocument(ref.current);
3437

3538
Streamlit.setComponentValue({
@@ -46,25 +49,25 @@ export const StDatePickerTrigger = forwardRef<
4649
useBodyStyle("body { padding-right: 0.5em !important; }");
4750

4851
return (
49-
<Popover>
50-
<PopoverTrigger asChild>
51-
<div className="m-1" ref={ref}>
52-
{label && <Label className="mb-2 block">{label}</Label>}
53-
<Button
54-
variant={"outline"}
55-
className={cn(
56-
"w-[280px] justify-start text-left font-normal",
57-
!date && "text-muted-foreground"
58-
)}
59-
onClick={() => {
60-
setOpen((v) => !v);
61-
}}
62-
>
63-
<CalendarIcon className="mr-2 h-4 w-4" />
64-
{date ? format(date, "PPP") : <span>Pick a date</span>}
65-
</Button>
66-
</div>
67-
</PopoverTrigger>
68-
</Popover>
52+
<div className="m-1" ref={ref}>
53+
{label && <Label className="mb-2 block">{label}</Label>}
54+
<Button
55+
variant={"outline"}
56+
className={cn(
57+
"w-[280px] justify-start text-left font-normal",
58+
!date && "text-muted-foreground"
59+
)}
60+
onClick={() => {
61+
setOpen((v) => !v);
62+
}}
63+
>
64+
<CalendarIcon className="mr-2 h-4 w-4" />
65+
{date ? (
66+
date.map((d) => format(d, "yyyy-MM-dd")).join(" - ")
67+
) : (
68+
<span>Pick a date</span>
69+
)}
70+
</Button>
71+
</div>
6972
);
7073
});

streamlit_shadcn_ui/py_components/button.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
# variant "default" | "destructive" | "outline" | "secondary" | "ghost" | "link"
1010

11-
1211
def button(text: str, variant: str = "default", class_name: str = None, key=None, **kwargs):
1312
props = {"text": text, "variant": variant, "className": class_name, **kwargs}
1413
default_state = init_default_state(key, default_value=False)

streamlit_shadcn_ui/py_components/date_picker.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def date_picker_trigger(value = None, label: str = None, open_status = False, ke
1111
props = {"value": value, "open": open_status, "label": label}
1212
return _component_func(comp=name, props=props, key=key, default={"x": 0, "y": 0, "open": False})
1313

14-
def date_picker_content(x: int, y: int, value=None, default_value=None, open: bool = False, key=None, on_change=None, args=None, kwargs=None):
14+
def date_picker_content(x: int, y: int, mode: str, value=None, default_value=None, open: bool = False, key=None, on_change=None, args=None, kwargs=None):
1515
name = "date_picker_content"
1616
_component_func = declare_component(name)
1717
register_callback(key=key, callback=on_change, args=args, kwargs=kwargs)
@@ -21,26 +21,26 @@ def date_picker_content(x: int, y: int, value=None, default_value=None, open: bo
2121
top: {y}px;
2222
left: {x}px;
2323
display: { "block" if open else "none" };
24+
background-color: transparent;
2425
z-index: 1000;
2526
}}
2627
""")
2728
with container:
28-
props = {"value": value}
29+
props = {"value": value, "mode": mode}
2930
result = _component_func(comp=name, props=props, key=key, default={"value": default_value, "open": False})
3031

3132
return result
3233

3334
def date_choosen_handler(from_key, to_key):
3435
st.session_state[to_key]['open'] = st.session_state[from_key]['open']
3536

36-
def date_picker(label=None, default_value=None, key=None):
37+
def date_picker(label=None, mode="single", default_value=None, key=None):
3738
trigger_component_key = f"trigger_{key}"
3839
content_component_key = f"content_{key}"
3940
init_session(key=trigger_component_key, default_value={"x": 0, "y": 0, "open": False})
4041
init_session(key=content_component_key, default_value={"value": default_value, "open": False})
4142
open_status = st.session_state[trigger_component_key]['open']
42-
with stylable_container(key=f"root_{key}", css_styles="""
43-
43+
with stylable_container(key=f"root_{key}", css_styles="""
4444
{
4545
position: relative;
4646
}
@@ -49,6 +49,6 @@ def date_picker(label=None, default_value=None, key=None):
4949
trigger_pos = date_picker_trigger(value=value, label=label, open_status=open_status, key=trigger_component_key)
5050
# need to sync value, or "cancel" will not work
5151
st.session_state[content_component_key]['open'] = trigger_pos['open']
52-
content_state = date_picker_content(value=value, x=trigger_pos['x'], y=trigger_pos['y'], open=open_status, key=content_component_key, on_change=date_choosen_handler, kwargs={"from_key": content_component_key, "to_key": trigger_component_key})
52+
content_state = date_picker_content(value=value, mode=mode, x=trigger_pos['x'], y=trigger_pos['y'], open=open_status, key=content_component_key, on_change=date_choosen_handler, kwargs={"from_key": content_component_key, "to_key": trigger_component_key})
5353
value = content_state['value']
5454
return value

0 commit comments

Comments
 (0)