leptos_shadcn_dialog/
default.rs

1use leptos::{ev::MouseEvent, prelude::*};
2use leptos_node_ref::AnyNodeRef;
3use leptos_struct_component::{StructComponent, struct_component};
4use leptos_style::Style;
5
6// Dialog Root Provider
7#[component]
8pub fn Dialog(
9    #[prop(into, optional)] open: Signal<bool>,
10    #[prop(into, optional)] on_open_change: Option<Callback<bool>>,
11    #[prop(optional)] children: Option<Children>,
12) -> impl IntoView {
13    let internal_open = RwSignal::new(false);
14    
15    let open_state = Signal::derive(move || {
16        if open.get() != internal_open.get() {
17            open.get()
18        } else {
19            internal_open.get()
20        }
21    });
22
23    let set_open = Callback::new(move |new_open: bool| {
24        internal_open.set(new_open);
25        if let Some(callback) = &on_open_change {
26            callback.run(new_open);
27        }
28    });
29
30    provide_context(DialogContextValue {
31        open: open_state,
32        set_open,
33    });
34
35    view! {
36        <div>
37            {children.map(|c| c())}
38        </div>
39    }
40}
41
42#[derive(Clone, Copy)]
43pub struct DialogContextValue {
44    pub open: Signal<bool>,
45    pub set_open: Callback<bool>,
46}
47
48// Dialog Trigger
49#[derive(Clone, StructComponent)]
50#[struct_component(tag = "button")]
51pub struct DialogTriggerChildProps {
52    pub node_ref: AnyNodeRef,
53    pub class: Signal<String>,
54    pub id: MaybeProp<String>,
55    pub style: Signal<Style>,
56    pub disabled: Signal<bool>,
57    pub r#type: MaybeProp<String>,
58    pub onclick: Option<Callback<MouseEvent>>,
59}
60
61#[component]
62pub fn DialogTrigger(
63    #[prop(into, optional)] class: MaybeProp<String>,
64    #[prop(into, optional)] id: MaybeProp<String>,
65    #[prop(into, optional)] style: Signal<Style>,
66    #[prop(into, optional)] node_ref: AnyNodeRef,
67    #[prop(into, optional)] as_child: Option<Callback<DialogTriggerChildProps, AnyView>>,
68    #[prop(optional)] children: Option<Children>,
69) -> impl IntoView {
70    let ctx = expect_context::<DialogContextValue>();
71    
72    let trigger_class = Signal::derive(move || {
73        format!("{}", class.get().unwrap_or_default())
74    });
75
76    let handle_click = Callback::new(move |_: MouseEvent| {
77        ctx.set_open.run(true);
78    });
79
80    let child_props = DialogTriggerChildProps {
81        node_ref,
82        class: trigger_class,
83        id,
84        style,
85        disabled: Signal::derive(|| false),
86        r#type: "button".to_string().into(),
87        onclick: Some(handle_click),
88    };
89
90    if let Some(as_child) = as_child.as_ref() {
91        as_child.run(child_props)
92    } else {
93        child_props.render(children)
94    }
95}
96
97// Dialog Content
98#[component]
99pub fn DialogContent(
100    #[prop(into, optional)] class: MaybeProp<String>,
101    #[prop(into, optional)] style: Signal<Style>,
102    #[prop(optional)] children: Option<Children>,
103) -> impl IntoView {
104    let ctx = expect_context::<DialogContextValue>();
105    
106    let content_class = Signal::derive(move || {
107        format!("fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg {}", class.get().unwrap_or_default())
108    });
109
110    if ctx.open.get() {
111        view! {
112            <div 
113                class="fixed inset-0 z-50"
114                on:click=move |_| ctx.set_open.run(false)
115            >
116                <div
117                    class={content_class}
118                    style={move || style.get().to_string()}
119                    on:click=|e: MouseEvent| e.stop_propagation()
120                >
121                    {children.map(|c| c())}
122                </div>
123            </div>
124        }.into_any()
125    } else {
126        view! { <div></div> }.into_any()
127    }
128}
129
130// Dialog Header
131#[component]
132pub fn DialogHeader(
133    #[prop(into, optional)] class: MaybeProp<String>,
134    #[prop(optional)] children: Option<Children>,
135) -> impl IntoView {
136    let header_class = Signal::derive(move || {
137        format!("flex flex-col space-y-1.5 text-center sm:text-left {}", class.get().unwrap_or_default())
138    });
139
140    view! {
141        <div class={header_class}>
142            {children.map(|c| c())}
143        </div>
144    }
145}
146
147// Dialog Title
148#[component]
149pub fn DialogTitle(
150    #[prop(into, optional)] class: MaybeProp<String>,
151    #[prop(optional)] children: Option<Children>,
152) -> impl IntoView {
153    let title_class = Signal::derive(move || {
154        format!("text-lg font-semibold leading-none tracking-tight {}", class.get().unwrap_or_default())
155    });
156
157    view! {
158        <h2 class={title_class}>
159            {children.map(|c| c())}
160        </h2>
161    }
162}
163
164// Dialog Description
165#[component]
166pub fn DialogDescription(
167    #[prop(into, optional)] class: MaybeProp<String>,
168    #[prop(optional)] children: Option<Children>,
169) -> impl IntoView {
170    let description_class = Signal::derive(move || {
171        format!("text-sm text-muted-foreground {}", class.get().unwrap_or_default())
172    });
173
174    view! {
175        <p class={description_class}>
176            {children.map(|c| c())}
177        </p>
178    }
179}
180
181// Dialog Footer
182#[component]
183pub fn DialogFooter(
184    #[prop(into, optional)] class: MaybeProp<String>,
185    #[prop(optional)] children: Option<Children>,
186) -> impl IntoView {
187    let footer_class = Signal::derive(move || {
188        format!("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 {}", class.get().unwrap_or_default())
189    });
190
191    view! {
192        <div class={footer_class}>
193            {children.map(|c| c())}
194        </div>
195    }
196}
197
198// Dialog Close
199#[component]
200pub fn DialogClose(
201    #[prop(into, optional)] class: MaybeProp<String>,
202    #[prop(optional)] children: Option<Children>,
203) -> impl IntoView {
204    let ctx = expect_context::<DialogContextValue>();
205    
206    let close_class = Signal::derive(move || {
207        format!("{}", class.get().unwrap_or_default())
208    });
209
210    view! {
211        <button
212            class={close_class}
213            on:click=move |_| ctx.set_open.run(false)
214        >
215            {children.map(|c| c())}
216        </button>
217    }
218}