browser_rs/
dioxus.rs

1#![doc = include_str!("../DIOXUS.md")]
2
3use crate::common::{ButtonType, Size, Variant};
4use dioxus::prelude::*;
5use gloo_timers::callback::Timeout;
6use std::rc::Rc;
7use web_sys::{
8    HtmlInputElement,
9    wasm_bindgen::{JsCast, prelude::*},
10    window,
11};
12
13#[derive(PartialEq, Props, Clone)]
14pub struct BrowserContentProps {
15    #[props(default)]
16    pub class: &'static str,
17    #[props(default)]
18    pub style: &'static str,
19    #[props(default = "Browser content area")]
20    pub aria_label: &'static str,
21    #[props(default)]
22    pub aria_describedby: &'static str,
23    children: Element,
24}
25
26#[component]
27pub fn BrowserContent(props: BrowserContentProps) -> Element {
28    rsx! {
29        main {
30            class: "{props.class}",
31            style: "{props.style}",
32            role: "main",
33            aria_label: "{props.aria_label}",
34            aria_describedby: "{props.aria_describedby}",
35            tabindex: "-1",
36            {props.children}
37        }
38    }
39}
40
41#[derive(PartialEq, Props, Clone)]
42pub struct AddressBarProps {
43    #[props(default)]
44    pub url: String,
45    #[props(default = "Enter URL or search...")]
46    pub placeholder: &'static str,
47    #[props(default)]
48    pub on_url_change: EventHandler<FormEvent>,
49    #[props(default)]
50    pub read_only: bool,
51    #[props(default)]
52    pub class: &'static str,
53    #[props(
54        default = "flex: 1; margin-left: 1rem; margin-right: 1rem; border: 1px solid #d1d5db; border-radius: 0.375rem; padding-left: 0.75rem; padding-right: 0.75rem; font-size: 0.875rem; position: relative;"
55    )]
56    pub style: &'static str,
57    #[props(default = "Website address or search query")]
58    pub label: &'static str,
59    #[props(default = "Enter a website URL or search term. Press Enter to navigate.")]
60    pub describedby: &'static str,
61    #[props(default = "browser-url-input")]
62    pub input_id: &'static str,
63    #[props(default = "text-black dark:text-white")]
64    pub input_class: &'static str,
65    #[props(default)]
66    pub container_class: &'static str,
67    #[props(
68        default = "position: absolute; top: 50%; right: 8px; transform: translateY(-50%); padding: 4px; background: none; border: none; box-shadow: none; outline: none; cursor: pointer;"
69    )]
70    pub refresh_button_style: &'static str,
71    #[props(default = "Refresh")]
72    pub refresh_button_aria_label: &'static str,
73    #[props(
74        default = "background-color: transparent; padding-right: 2rem; border: none; outline: none; box-shadow: none; height: 100%;"
75    )]
76    pub input_style: &'static str,
77}
78
79#[component]
80pub fn AddressBar(props: AddressBarProps) -> Element {
81    let mut input_value = use_signal(|| props.url.clone());
82    let mut is_focused = use_signal(|| false);
83    let mut input_ref: Signal<Option<Rc<MountedData>>> = use_signal(|| None);
84
85    use_effect(move || {
86        input_value.set(props.url.clone());
87    });
88
89    let on_input_change = move |evt: FormEvent| {
90        input_value.set(evt.value());
91        props.on_url_change.call(evt);
92    };
93    let on_key_down = move |evt: Event<KeyboardData>| {
94        if evt.key() == Key::Enter {
95            evt.prevent_default();
96
97            if let Some(node) = &*input_ref.read() {
98                if let Some(input) = node.downcast::<HtmlInputElement>() {
99                    let _ = input.blur();
100                }
101            }
102
103            if let Some(document) = window().and_then(|w| w.document()) {
104                let live_region = document.create_element("div").unwrap();
105                live_region.set_attribute("aria-live", "polite").unwrap();
106                live_region.set_attribute("aria-atomic", "true").unwrap();
107                live_region.set_class_name("sr-only");
108                live_region.set_attribute("style", "position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;").unwrap();
109                live_region
110                    .set_text_content(Some(&format!("Navigating to {}", input_value.read())));
111                document.body().unwrap().append_child(&live_region).unwrap();
112
113                let clone = live_region.clone();
114                Timeout::new(1000, move || {
115                    let _ = document.body().unwrap().remove_child(&clone);
116                })
117                .forget();
118            }
119        }
120    };
121
122    rsx! {
123        div {
124            class: "{props.container_class} {props.class}",
125            style: "{props.style}",
126            label {
127                r#for: "{props.input_id}",
128                class: "sr-only",
129                style: "position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;",
130                "{props.label}"
131            }
132            input {
133                id: "{props.input_id}",
134                r#type: "text",
135                class: "{props.input_class}",
136                style: "{props.input_style}",
137                value: "{input_value}",
138                oninput: on_input_change,
139                onkeydown: on_key_down,
140                onfocus: move |_| is_focused.set(true),
141                onblur: move |_| is_focused.set(false),
142                placeholder: "{props.placeholder}",
143                readonly: props.read_only,
144                aria_describedby: "{props.describedby}",
145                autocomplete: "url",
146                spellcheck: "false",
147                onmounted: move |cx| input_ref.set(Some(cx.data())),
148            }
149            button {
150                style: "{props.refresh_button_style}",
151                aria_label: "{props.refresh_button_aria_label}",
152                onclick: move |_| {
153                    let _ = window().unwrap().location().reload();
154                },
155                svg {
156                    width: "11",
157                    height: "13",
158                    view_box: "0 0 11 13",
159                    fill: "none",
160                    xmlns: "http://www.w3.org/2000/svg",
161                    path {
162                        d: "M4.99385 1.00002L7.33006 3.33623L4.99385 5.67244M10 7.61925C10 10.1998 7.9081 12.2917 5.3276 12.2917C2.74709 12.2917 0.655182 10.1998 0.655182 7.61925C0.655182 5.03875 2.74709 2.94684 5.3276 2.94684C5.8737 2.94684 6.4957 2.94684 7.27443 3.33621",
163                        stroke: "#767676",
164                        stroke_linecap: "round",
165                        stroke_linejoin: "round"
166                    }
167                }
168            }
169        }
170    }
171}
172
173#[derive(Props, PartialEq, Clone)]
174pub struct ControlButtonProps {
175    pub r#type: ButtonType,
176    #[props(default)]
177    pub on_click: EventHandler<()>,
178    #[props(default)]
179    pub on_mouse_over: EventHandler<()>,
180    #[props(default)]
181    pub on_mouse_out: EventHandler<()>,
182    #[props(default)]
183    pub on_focus: EventHandler<FocusEvent>,
184    #[props(default)]
185    pub on_blur: EventHandler<FocusEvent>,
186    #[props(
187        default = "width: 1rem; height: 1rem; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; cursor: pointer; background: none; border: none; padding: 0; margin-right: 0.5rem;"
188    )]
189    pub style: &'static str,
190    #[props(default)]
191    pub class: &'static str,
192    #[props(default)]
193    pub svg_class: &'static str,
194    #[props(default)]
195    pub path_class: &'static str,
196    #[props(default = "button")]
197    pub button_type: &'static str,
198    #[props(default)]
199    pub aria_label: &'static str,
200    #[props(default)]
201    pub title: &'static str,
202    #[props(default = "0")]
203    pub tabindex: &'static str,
204}
205
206#[component]
207pub fn ControlButton(props: ControlButtonProps) -> Element {
208    let (fill, stroke) = match props.r#type {
209        ButtonType::Close => ("#FF5F57", "#E14640"),
210        ButtonType::Minimize => ("#FFBD2E", "#DFA123"),
211        ButtonType::Maximize => ("#28CA42", "#1DAD2C"),
212    };
213
214    let aria_label = if props.aria_label.is_empty() {
215        props.r#type.default_aria_label()
216    } else {
217        props.aria_label
218    };
219
220    let title = if props.title.is_empty() {
221        props.r#type.default_title()
222    } else {
223        props.title
224    };
225
226    rsx! {
227        button {
228            r#type: "{props.button_type}",
229            class: "{props.class}",
230            style: "{props.style}",
231            aria_label: "{aria_label}",
232            title: "{title}",
233            tabindex: "{props.tabindex}",
234            onclick: move |_| props.on_click.call(()),
235            onmouseover: move |_| props.on_mouse_over.call(()),
236            onmouseout: move |_| props.on_mouse_out.call(()),
237            onfocus: props.on_focus,
238            onblur: props.on_blur,
239
240            svg {
241                class: "{props.svg_class}",
242                width: "12",
243                height: "12",
244                view_box: "0 0 12 12",
245                fill: "none",
246                xmlns: "http://www.w3.org/2000/svg",
247                path {
248                    class: "{props.path_class}",
249                    d: "M6 0.5C9.03757 0.5 11.5 2.96243 11.5 6C11.5 9.03757 9.03757 11.5 6 11.5C2.96243 11.5 0.5 9.03757 0.5 6C0.5 2.96243 2.96243 0.5 6 0.5Z",
250                    fill: "{fill}",
251                    stroke: "{stroke}"
252                }
253            }
254        }
255    }
256}
257
258#[derive(PartialEq, Props, Clone)]
259pub struct BrowserControlsProps {
260    #[props(default)]
261    pub show_controls: bool,
262    #[props(default)]
263    pub class: &'static str,
264    #[props(default = "display: flex; align-items: center; background: none; padding-left: 10px;")]
265    pub style: &'static str,
266
267    #[props(default)]
268    pub on_close: EventHandler<()>,
269    #[props(default)]
270    pub on_close_mouse_over: EventHandler<()>,
271    #[props(default)]
272    pub on_close_mouse_out: EventHandler<()>,
273    #[props(default)]
274    pub on_close_focus: EventHandler<FocusEvent>,
275    #[props(default)]
276    pub on_close_blur: EventHandler<FocusEvent>,
277    #[props(default)]
278    pub close_class: &'static str,
279    #[props(default)]
280    pub close_svg_class: &'static str,
281    #[props(default)]
282    pub close_path_class: &'static str,
283    #[props(default = "button")]
284    pub close_button_type: &'static str,
285    #[props(default)]
286    pub close_aria_label: &'static str,
287    #[props(default)]
288    pub close_title: &'static str,
289    #[props(default = "0")]
290    pub close_tabindex: &'static str,
291
292    #[props(default)]
293    pub on_minimize: EventHandler<()>,
294    #[props(default)]
295    pub on_minimize_mouse_over: EventHandler<()>,
296    #[props(default)]
297    pub on_minimize_mouse_out: EventHandler<()>,
298    #[props(default)]
299    pub on_minimize_focus: EventHandler<FocusEvent>,
300    #[props(default)]
301    pub on_minimize_blur: EventHandler<FocusEvent>,
302    #[props(default)]
303    pub minimize_class: &'static str,
304    #[props(default)]
305    pub minimize_svg_class: &'static str,
306    #[props(default)]
307    pub minimize_path_class: &'static str,
308    #[props(default = "button")]
309    pub minimize_button_type: &'static str,
310    #[props(default)]
311    pub minimize_aria_label: &'static str,
312    #[props(default)]
313    pub minimize_title: &'static str,
314    #[props(default = "0")]
315    pub minimize_tabindex: &'static str,
316
317    #[props(default)]
318    pub on_maximize: EventHandler<()>,
319    #[props(default)]
320    pub on_maximize_mouse_over: EventHandler<()>,
321    #[props(default)]
322    pub on_maximize_mouse_out: EventHandler<()>,
323    #[props(default)]
324    pub on_maximize_focus: EventHandler<FocusEvent>,
325    #[props(default)]
326    pub on_maximize_blur: EventHandler<FocusEvent>,
327    #[props(default)]
328    pub maximize_class: &'static str,
329    #[props(default)]
330    pub maximize_svg_class: &'static str,
331    #[props(default)]
332    pub maximize_path_class: &'static str,
333    #[props(default = "button")]
334    pub maximize_button_type: &'static str,
335    #[props(default)]
336    pub maximize_aria_label: &'static str,
337    #[props(default)]
338    pub maximize_title: &'static str,
339    #[props(default = "0")]
340    pub maximize_tabindex: &'static str,
341}
342
343#[component]
344pub fn BrowserControls(props: BrowserControlsProps) -> Element {
345    if !props.show_controls {
346        return rsx! {};
347    }
348
349    rsx! {
350        nav {
351            class: "{props.class}",
352            style: "{props.style}",
353            role: "toolbar",
354            aria_label: "Browser window controls",
355            ControlButton {
356                r#type: ButtonType::Close,
357                on_click: props.on_close,
358                on_mouse_over: props.on_close_mouse_over,
359                on_mouse_out: props.on_close_mouse_out,
360                on_focus: props.on_close_focus,
361                on_blur: props.on_close_blur,
362                class: props.close_class,
363                svg_class: props.close_svg_class,
364                path_class: props.close_path_class,
365                button_type: props.close_button_type,
366                aria_label: props.close_aria_label,
367                title: props.close_title,
368                tabindex: props.close_tabindex,
369            }
370            ControlButton {
371                r#type: ButtonType::Minimize,
372                on_click: props.on_minimize,
373                on_mouse_over: props.on_minimize_mouse_over,
374                on_mouse_out: props.on_minimize_mouse_out,
375                on_focus: props.on_minimize_focus,
376                on_blur: props.on_minimize_blur,
377                class: props.minimize_class,
378                svg_class: props.minimize_svg_class,
379                path_class: props.minimize_path_class,
380                button_type: props.minimize_button_type,
381                aria_label: props.minimize_aria_label,
382                title: props.minimize_title,
383                tabindex: props.minimize_tabindex,
384            }
385            ControlButton {
386                r#type: ButtonType::Maximize,
387                on_click: props.on_maximize,
388                on_mouse_over: props.on_maximize_mouse_over,
389                on_mouse_out: props.on_maximize_mouse_out,
390                on_focus: props.on_maximize_focus,
391                on_blur: props.on_maximize_blur,
392                class: props.maximize_class,
393                svg_class: props.maximize_svg_class,
394                path_class: props.maximize_path_class,
395                button_type: props.maximize_button_type,
396                aria_label: props.maximize_aria_label,
397                title: props.maximize_title,
398                tabindex: props.maximize_tabindex,
399            }
400        }
401    }
402}
403
404#[derive(PartialEq, Props, Clone)]
405pub struct BrowserHeaderProps {
406    #[props(default)]
407    pub url: String,
408    #[props(default)]
409    pub placeholder: &'static str,
410    #[props(default)]
411    pub on_url_change: Option<EventHandler<FormEvent>>,
412    #[props(default = true)]
413    pub show_controls: bool,
414    #[props(default = true)]
415    pub show_address_bar: bool,
416    #[props(default = false)]
417    pub read_only: bool,
418    #[props(default)]
419    pub variant: Variant,
420    #[props(default)]
421    pub size: Size,
422    #[props(default)]
423    pub custom_buttons: Vec<Element>,
424    #[props(default)]
425    pub class: &'static str,
426
427    #[props(default)]
428    pub container_class: &'static str,
429    #[props(default = "text-black dark:text-white")]
430    pub input_class: &'static str,
431    #[props(default)]
432    pub refresh_button_style: &'static str,
433    #[props(default = "Refresh")]
434    pub refresh_button_aria_label: &'static str,
435
436    #[props(
437        default = "padding: 4px; cursor: pointer; background: none; border: none; box-shadow: none; outline: none;"
438    )]
439    pub icon_button_style: &'static str,
440
441    #[props(default = "flex: 1; display: flex; justify-content: center; padding-right: 8px;")]
442    pub address_wrapper_base_style: &'static str,
443
444    #[props(default = "display: flex; align-items: center; position: relative;")]
445    pub header_base_style: &'static str,
446
447    #[props(default)]
448    pub on_close: EventHandler<()>,
449    #[props(default)]
450    pub on_close_mouse_over: EventHandler<()>,
451    #[props(default)]
452    pub on_close_mouse_out: EventHandler<()>,
453    #[props(default)]
454    pub on_close_focus: EventHandler<FocusEvent>,
455    #[props(default)]
456    pub on_close_blur: EventHandler<FocusEvent>,
457    #[props(default)]
458    pub close_class: &'static str,
459    #[props(default)]
460    pub close_svg_class: &'static str,
461    #[props(default)]
462    pub close_path_class: &'static str,
463    #[props(default = "button")]
464    pub close_button_type: &'static str,
465    #[props(default)]
466    pub close_aria_label: &'static str,
467    #[props(default)]
468    pub close_title: &'static str,
469    #[props(default = "0")]
470    pub close_tabindex: &'static str,
471
472    #[props(default)]
473    pub on_minimize: EventHandler<()>,
474    #[props(default)]
475    pub on_minimize_mouse_over: EventHandler<()>,
476    #[props(default)]
477    pub on_minimize_mouse_out: EventHandler<()>,
478    #[props(default)]
479    pub on_minimize_focus: EventHandler<FocusEvent>,
480    #[props(default)]
481    pub on_minimize_blur: EventHandler<FocusEvent>,
482    #[props(default)]
483    pub minimize_class: &'static str,
484    #[props(default)]
485    pub minimize_svg_class: &'static str,
486    #[props(default)]
487    pub minimize_path_class: &'static str,
488    #[props(default = "button")]
489    pub minimize_button_type: &'static str,
490    #[props(default)]
491    pub minimize_aria_label: &'static str,
492    #[props(default)]
493    pub minimize_title: &'static str,
494    #[props(default = "0")]
495    pub minimize_tabindex: &'static str,
496
497    #[props(default)]
498    pub on_maximize: EventHandler<()>,
499    #[props(default)]
500    pub on_maximize_mouse_over: EventHandler<()>,
501    #[props(default)]
502    pub on_maximize_mouse_out: EventHandler<()>,
503    #[props(default)]
504    pub on_maximize_focus: EventHandler<FocusEvent>,
505    #[props(default)]
506    pub on_maximize_blur: EventHandler<FocusEvent>,
507    #[props(default)]
508    pub maximize_class: &'static str,
509    #[props(default)]
510    pub maximize_svg_class: &'static str,
511    #[props(default)]
512    pub maximize_path_class: &'static str,
513    #[props(default = "button")]
514    pub maximize_button_type: &'static str,
515    #[props(default)]
516    pub maximize_aria_label: &'static str,
517    #[props(default)]
518    pub maximize_title: &'static str,
519    #[props(default = "0")]
520    pub maximize_tabindex: &'static str,
521
522    #[props(default)]
523    pub share_button_style: &'static str,
524    #[props(default)]
525    pub share_onclick: EventHandler<()>,
526    #[props(default)]
527    pub share_onmouseover: EventHandler<()>,
528    #[props(default)]
529    pub share_onmouseout: EventHandler<()>,
530    #[props(default)]
531    pub share_onfocus: EventHandler<FocusEvent>,
532    #[props(default)]
533    pub share_onblur: EventHandler<FocusEvent>,
534    #[props(default)]
535    pub share_tabindex: &'static str,
536
537    #[props(default)]
538    pub tabs_button_style: &'static str,
539    #[props(default)]
540    pub tabs_onclick: EventHandler<()>,
541    #[props(default)]
542    pub tabs_onmouseover: EventHandler<()>,
543    #[props(default)]
544    pub tabs_onmouseout: EventHandler<()>,
545    #[props(default)]
546    pub tabs_onfocus: EventHandler<FocusEvent>,
547    #[props(default)]
548    pub tabs_onblur: EventHandler<FocusEvent>,
549    #[props(default)]
550    pub tabs_tabindex: &'static str,
551
552    #[props(default)]
553    pub more_button_style: &'static str,
554    #[props(default)]
555    pub more_onclick: EventHandler<()>,
556    #[props(default)]
557    pub more_onmouseover: EventHandler<()>,
558    #[props(default)]
559    pub more_onmouseout: EventHandler<()>,
560    #[props(default)]
561    pub more_onfocus: EventHandler<FocusEvent>,
562    #[props(default)]
563    pub more_onblur: EventHandler<FocusEvent>,
564    #[props(default)]
565    pub more_tabindex: &'static str,
566}
567
568#[component]
569pub fn BrowserHeader(props: BrowserHeaderProps) -> Element {
570    let is_ios = props.variant == Variant::Ios;
571    let is_tabs = props.variant == Variant::Tabs;
572
573    let base_style = {
574        let padding = match props.size {
575            Size::Small => "4px 6px",
576            Size::Large => "10px 16px",
577            _ => "6px 12px",
578        };
579        let height = match (props.variant.clone(), props.size.clone()) {
580            (Variant::Tabs, _) => "40px",
581            (Variant::Ios, _) => "56px",
582            (_, Size::Large) => "60px",
583            (_, Size::Small) => "38px",
584            _ => "48px",
585        };
586        let border_radius = if is_tabs {
587            "6px"
588        } else if props.variant == Variant::Default {
589            "8px 8px 0 0"
590        } else {
591            "0"
592        };
593        let border = if is_tabs { "1px solid #d1d5db" } else { "none" };
594        let box_shadow = if props.variant == Variant::Default {
595            "0 2px 6px rgba(0,0,0,0.1)"
596        } else {
597            "none"
598        };
599
600        format!(
601            "{} justify-content: {}; padding: {}; height: {}; border-radius: {}; border: {}; box-shadow: {};",
602            props.header_base_style,
603            if is_ios {
604                "space-between"
605            } else {
606                "flex-start"
607            },
608            padding,
609            height,
610            border_radius,
611            border,
612            box_shadow
613        )
614    };
615
616    let address_wrapper_style = format!(
617        "{} padding-left: {};",
618        props.address_wrapper_base_style,
619        if props.show_controls { "8px" } else { "0" }
620    );
621
622    let share_onclick = move |_| props.share_onclick.call(());
623    let share_onmouseover = move |_| props.share_onmouseover.call(());
624    let share_onmouseout = move |_| props.share_onmouseout.call(());
625
626    let tabs_onclick = move |_| props.tabs_onclick.call(());
627    let tabs_onmouseover = move |_| props.tabs_onmouseover.call(());
628    let tabs_onmouseout = move |_| props.tabs_onmouseout.call(());
629
630    let more_onclick = move |_| props.more_onclick.call(());
631    let more_onmouseover = move |_| props.more_onmouseover.call(());
632    let more_onmouseout = move |_| props.more_onmouseout.call(());
633
634    rsx! {
635        header {
636            style: "{base_style}",
637            class: "{props.class}",
638            "aria-label": "Browser window header",
639
640            div {
641                style: "display: flex; align-items: center; gap: 6px;",
642                if props.show_controls {
643                    BrowserControls {
644                        on_close: props.on_close,
645                        on_minimize: props.on_minimize,
646                        on_maximize: props.on_maximize,
647                        show_controls: props.show_controls,
648                        on_close_mouse_over: props.on_close_mouse_over,
649                        on_close_mouse_out: props.on_close_mouse_out,
650                        on_close_focus: props.on_close_focus,
651                        on_close_blur: props.on_close_blur,
652                        close_class: props.close_class,
653                        close_svg_class: props.close_svg_class,
654                        close_path_class: props.close_path_class,
655                        close_button_type: props.close_button_type,
656                        close_aria_label: props.close_aria_label,
657                        close_title: props.close_title,
658                        close_tabindex: props.close_tabindex,
659                        on_minimize_mouse_over: props.on_minimize_mouse_over,
660                        on_minimize_mouse_out: props.on_minimize_mouse_out,
661                        on_minimize_focus: props.on_minimize_focus,
662                        on_minimize_blur: props.on_minimize_blur,
663                        minimize_class: props.minimize_class,
664                        minimize_svg_class: props.minimize_svg_class,
665                        minimize_path_class: props.minimize_path_class,
666                        minimize_button_type: props.minimize_button_type,
667                        minimize_aria_label: props.minimize_aria_label,
668                        minimize_title: props.minimize_title,
669                        minimize_tabindex: props.minimize_tabindex,
670                        on_maximize_mouse_over: props.on_maximize_mouse_over,
671                        on_maximize_mouse_out: props.on_maximize_mouse_out,
672                        on_maximize_focus: props.on_maximize_focus,
673                        on_maximize_blur: props.on_maximize_blur,
674                        maximize_class: props.maximize_class,
675                        maximize_svg_class: props.maximize_svg_class,
676                        maximize_path_class: props.maximize_path_class,
677                        maximize_button_type: props.maximize_button_type,
678                        maximize_aria_label: props.maximize_aria_label,
679                        maximize_title: props.maximize_title,
680                        maximize_tabindex: props.maximize_tabindex,
681                    }
682                    if !is_ios {
683                        button {
684                            style: "{props.icon_button_style}",
685                            "aria-label": "Sidebar",
686                            svg {
687                                width: "20",
688                                height: "15",
689                                view_box: "0 0 20 15",
690                                fill: "none",
691                                xmlns: "http://www.w3.org/2000/svg",
692                                path {
693                                    d: "M2.62346 15H16.4609C18.2202 15 19.0844 14.1358 19.0844 12.4074V2.59259C19.0844 0.864204 18.2202 0 16.4609 0H2.62346C0.874483 0 0 0.864204 0 2.59259V12.4074C0 14.1358 0.874483 15 2.62346 15ZM2.64404 13.5082C1.90329 13.5082 1.48149 13.1173 1.48149 12.3354V2.66461C1.48149 1.89301 1.90329 1.49177 2.64404 1.49177H6.22427V13.5082H2.64404ZM16.4403 1.49177C17.1811 1.49177 17.6029 1.89301 17.6029 2.66461V12.3354C17.6029 13.1173 17.1811 13.5082 16.4403 13.5082H7.67489V1.49177H16.4403ZM4.67078 4.47532C4.94857 4.47532 5.18518 4.2284 5.18518 3.9609C5.18518 3.69341 4.94857 3.46708 4.67078 3.46708H3.05556C2.78806 3.46708 2.55144 3.69341 2.55144 3.9609C2.55144 4.2284 2.78806 4.47532 3.05556 4.47532H4.67078ZM4.67078 6.53293C4.94857 6.53293 5.18518 6.29629 5.18518 6.01853C5.18518 5.75102 4.94857 5.52469 4.67078 5.52469H3.05556C2.78806 5.52469 2.55144 5.75102 2.55144 6.01853C2.55144 6.29629 2.78806 6.53293 3.05556 6.53293H4.67078ZM4.67078 8.59054C4.94857 8.59054 5.18518 8.35392 5.18518 8.08642C5.18518 7.81893 4.94857 7.5926 4.67078 7.5926H3.05556C2.78806 7.5926 2.55144 7.81893 2.55144 8.08642C2.55144 8.35392 2.78806 8.59054 3.05556 8.59054H4.67078Z",
694                                    fill: "#767676",
695                                }
696                            }
697                        }
698                        button {
699                            style: "{props.icon_button_style}",
700                            "aria-label": "Back",
701                            svg {
702                                width: "9",
703                                height: "16",
704                                view_box: "0 0 9 16",
705                                fill: "none",
706                                xmlns: "http://www.w3.org/2000/svg",
707                                path {
708                                    d: "M7.5 1.5L1 8L7.5 14.5",
709                                    stroke: "#737373",
710                                    stroke_width: "1.5",
711                                    stroke_linecap: "round",
712                                    stroke_linejoin: "round",
713                                }
714                            }
715                        }
716                        button {
717                            style: "{props.icon_button_style}",
718                            "aria-label": "Forward",
719                            svg {
720                                width: "9",
721                                height: "16",
722                                view_box: "0 0 9 16",
723                                fill: "none",
724                                xmlns: "http://www.w3.org/2000/svg",
725                                path {
726                                    d: "M1 14.5L7.5 8L1 1.5",
727                                    stroke: "#BFBFBF",
728                                    stroke_width: "1.5",
729                                    stroke_linecap: "round",
730                                    stroke_linejoin: "round",
731                                }
732                            }
733                        }
734                    }
735                }
736            }
737
738            if props.show_address_bar {
739                div {
740                    style: "{address_wrapper_style}",
741                    AddressBar {
742                        url: props.url,
743                        placeholder: props.placeholder,
744                        on_url_change: props.on_url_change.unwrap_or_default(),
745                        read_only: props.read_only,
746                        input_class: props.input_class,
747                        container_class: props.container_class,
748                        refresh_button_style: props.refresh_button_style,
749                        refresh_button_aria_label: props.refresh_button_aria_label,
750                    }
751                }
752            }
753
754            div {
755                style: "display: flex; align-items: center; gap: 6px; margin-left: auto;",
756                if props.show_controls {
757                    for btn in &props.custom_buttons {
758                        {btn}
759                    }
760                    button {
761                        style: "{props.icon_button_style}",
762                        onclick: share_onclick,
763                        onmouseover: share_onmouseover,
764                        onmouseout: share_onmouseout,
765                        onfocus: props.share_onfocus,
766                        onblur: props.share_onblur,
767                        "aria-label": "Share",
768                        title: "Share",
769                        tabindex: "{props.share_tabindex}",
770                        svg {
771                            width: "15",
772                            height: "19",
773                            view_box: "0 0 15 19",
774                            fill: "none",
775                            xmlns: "http://www.w3.org/2000/svg",
776                            path {
777                                d: "M7.49467 12.3969C7.91045 12.3969 8.26225 12.056 8.26225 11.6513V3.34416L8.1983 2.06613L8.64605 2.55604L9.81876 3.82343C9.95736 3.97254 10.1493 4.04709 10.3305 4.04709C10.7356 4.04709 11.0341 3.77017 11.0341 3.38676C11.0341 3.17377 10.9488 3.02467 10.7996 2.88621L8.04905 0.255589C7.85715 0.0638861 7.69722 0 7.49467 0C7.30277 0 7.14286 0.0638861 6.94029 0.255589L4.18977 2.88621C4.05117 3.02467 3.96589 3.17377 3.96589 3.38676C3.96589 3.77017 4.25372 4.04709 4.65885 4.04709C4.84009 4.04709 5.04264 3.97254 5.18124 3.82343L6.35395 2.55604L6.80171 2.06613L6.73774 3.34416V11.6513C6.73774 12.056 7.08955 12.3969 7.49467 12.3969ZM2.71855 19H12.2814C14.1045 19 15 18.1054 15 16.3161V8.12611C15 6.33688 14.1045 5.44225 12.2814 5.44225H9.98934V6.98654H12.2601C13.0171 6.98654 13.4648 7.4019 13.4648 8.20066V16.2416C13.4648 17.051 13.0171 17.4557 12.2601 17.4557H2.73988C1.97228 17.4557 1.53519 17.051 1.53519 16.2416V8.20066C1.53519 7.4019 1.97228 6.98654 2.73988 6.98654H5.01065V5.44225H2.71855C0.906181 5.44225 0 6.33688 0 8.12611V16.3161C0 18.1054 0.906181 19 2.71855 19Z",
778                                fill: "#767676",
779                            }
780                        }
781                    }
782                    button {
783                        style: "{props.icon_button_style}",
784                        onclick: tabs_onclick,
785                        onmouseover: tabs_onmouseover,
786                        onmouseout: tabs_onmouseout,
787                        onfocus: props.tabs_onfocus,
788                        onblur: props.tabs_onblur,
789                        "aria-label": "Tabs",
790                        title: "Tabs",
791                        tabindex: "{props.tabs_tabindex}",
792                        svg {
793                            width: "15",
794                            height: "15",
795                            view_box: "0 0 15 15",
796                            fill: "none",
797                            xmlns: "http://www.w3.org/2000/svg",
798                            path {
799                                d: "M7.01662 14.6401C7.4887 14.6401 7.87493 14.2646 7.87493 13.7925V8.3745H13.1642C13.6255 8.3745 14.0225 7.97755 14.0225 7.50547C14.0225 7.03341 13.6255 6.63643 13.1642 6.63643H7.87493V1.20768C7.87493 0.735619 7.4887 0.360107 7.01662 0.360107C6.54456 0.360107 6.14758 0.735619 6.14758 1.20768V6.63643H0.869031C0.396973 6.63643 0 7.03341 0 7.50547C0 7.97755 0.396973 8.3745 0.869031 8.3745H6.14758V13.7925C6.14758 14.2646 6.54456 14.6401 7.01662 14.6401Z",
800                                fill: "#767676",
801                            }
802                        }
803                    }
804                    button {
805                        style: "{props.icon_button_style}",
806                        onclick: more_onclick,
807                        onmouseover: more_onmouseover,
808                        onmouseout: more_onmouseout,
809                        onfocus: props.more_onfocus,
810                        onblur: props.more_onblur,
811                        "aria-label": "More options",
812                        title: "More options",
813                        tabindex: "{props.more_tabindex}",
814                        svg {
815                            width: "18",
816                            height: "19",
817                            view_box: "0 0 18 19",
818                            fill: "none",
819                            xmlns: "http://www.w3.org/2000/svg",
820                            path {
821                                d: "M2.67776 14.2898H3.97934V15.5914C3.97934 17.3407 4.85401 18.205 6.63458 18.205H14.8189C16.5891 18.205 17.4742 17.3407 17.4742 15.5914V7.32373C17.4742 5.5744 16.5891 4.71016 14.8189 4.71016H13.5174V3.40857C13.5174 1.65923 12.6323 0.794983 10.8621 0.794983H2.67776C0.897191 0.794983 0.022522 1.65923 0.022522 3.40857V11.6762C0.022522 13.4256 0.897191 14.2898 2.67776 14.2898ZM2.69859 12.7904C1.94886 12.7904 1.52195 12.3843 1.52195 11.5929V3.49187C1.52195 2.70051 1.94886 2.29442 2.69859 2.29442H10.8413C11.591 2.29442 12.0179 2.70051 12.0179 3.49187V4.71016H6.63458C4.85401 4.71016 3.97934 5.5744 3.97934 7.32373V12.7904H2.69859ZM6.65539 16.7056C5.90568 16.7056 5.47878 16.2995 5.47878 15.5081V7.40704C5.47878 6.61567 5.90568 6.20957 6.65539 6.20957H14.7981C15.5478 6.20957 15.9747 6.61567 15.9747 7.40704V15.5081C15.9747 16.2995 15.5478 16.7056 14.7981 16.7056H6.65539Z",
822                                fill: "#767676",
823                            }
824                        }
825                    }
826                }
827            }
828        }
829    }
830}
831
832#[derive(Clone, PartialEq, Props)]
833pub struct KeyboardNavigationOptions {
834    pub on_escape: Option<EventHandler<()>>,
835    pub on_enter: Option<EventHandler<()>>,
836    pub trap_focus: bool,
837}
838
839pub fn use_keyboard(options: KeyboardNavigationOptions) -> Signal<Option<Rc<MountedData>>> {
840    let container_ref: Signal<Option<Rc<MountedData>>> = use_signal(|| None);
841
842    {
843        let options = options.clone();
844
845        use_effect(move || {
846            let closure = Closure::<dyn Fn(web_sys::KeyboardEvent)>::wrap(Box::new(
847                move |event: web_sys::KeyboardEvent| {
848                    let key = event.key();
849                    let target = event.target();
850
851                    match key.as_str() {
852                        "Escape" => {
853                            if let Some(callback) = &options.on_escape {
854                                event.prevent_default();
855                                callback.call(());
856                            }
857                        }
858                        "Enter" => {
859                            if let Some(callback) = &options.on_enter {
860                                if let Some(_target_elem) = target
861                                    .clone()
862                                    .and_then(|t| t.dyn_into::<web_sys::Element>().ok())
863                                {
864                                    if let Some(container) = container_ref
865                                        .read()
866                                        .as_ref()
867                                        .and_then(|r| r.downcast::<web_sys::Element>())
868                                    {
869                                        if target.unwrap() == ***container {
870                                            event.prevent_default();
871                                            callback.call(());
872                                        }
873                                    }
874                                }
875                            }
876                        }
877                        "Tab" if options.trap_focus => {
878                            if let Some(container) = container_ref
879                                .read()
880                                .as_ref()
881                                .and_then(|r| r.downcast::<web_sys::Element>())
882                            {
883                                let selector = "button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])";
884                                let focusables = container.query_selector_all(selector).unwrap();
885
886                                let length = focusables.length();
887                                if length == 0 {
888                                    return;
889                                }
890
891                                let first = focusables
892                                    .item(0)
893                                    .and_then(|e| e.dyn_into::<web_sys::HtmlElement>().ok());
894                                let last = focusables
895                                    .item(length - 1)
896                                    .and_then(|e| e.dyn_into::<web_sys::HtmlElement>().ok());
897
898                                let document = web_sys::window().unwrap().document().unwrap();
899                                let active = document.active_element();
900
901                                if event.shift_key() {
902                                    if active == first.as_ref().map(|e| e.clone().into()) {
903                                        event.prevent_default();
904                                        if let Some(elem) = last {
905                                            elem.focus().ok();
906                                        }
907                                    }
908                                } else if active == last.as_ref().map(|e| e.clone().into()) {
909                                    event.prevent_default();
910                                    if let Some(elem) = first {
911                                        elem.focus().ok();
912                                    }
913                                }
914                            }
915                        }
916                        _ => {}
917                    }
918                },
919            )
920                as Box<dyn Fn(web_sys::KeyboardEvent)>);
921
922            web_sys::window()
923                .unwrap()
924                .add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref())
925                .unwrap();
926        });
927    }
928
929    container_ref
930}
931/// Properties for the `BrowserFrame` component.
932///
933/// This component simulates a web browser window with controls like
934/// close, minimize, maximize, and supports rich customization via props.
935#[derive(PartialEq, Props, Clone)]
936pub struct BrowserFrameProps {
937    /// Child elements rendered inside the browser frame body.
938    #[props(default)]
939    pub children: Element,
940
941    /// The URL displayed in the address bar and used in the iframe.
942    #[props(default)]
943    pub url: String,
944
945    /// Placeholder text shown in the address input field.
946    #[props(default)]
947    pub placeholder: &'static str,
948
949    /// Event handler for when the address bar URL changes.
950    #[props(default)]
951    pub on_url_change: Option<EventHandler<FormEvent>>,
952
953    /// Event handler triggered when the close button is clicked.
954    #[props(default)]
955    pub on_close: EventHandler<()>,
956
957    /// Event handler triggered when the minimize button is clicked.
958    #[props(default)]
959    pub on_minimize: EventHandler<()>,
960
961    /// Event handler triggered when the maximize button is clicked.
962    #[props(default)]
963    pub on_maximize: EventHandler<()>,
964
965    /// Whether to show the top-right control buttons (close, minimize, maximize).
966    ///
967    /// Defaults to `true`.
968    #[props(default = true)]
969    pub show_controls: bool,
970
971    /// Whether to show the address bar.
972    ///
973    /// Defaults to `true`.
974    #[props(default = true)]
975    pub show_address_bar: bool,
976
977    /// Whether the address bar is read-only.
978    ///
979    /// Defaults to `false`.
980    #[props(default = false)]
981    pub read_only: bool,
982
983    /// Size of the browser frame container.
984    #[props(default)]
985    pub size: Size,
986
987    /// Display variant for the frame (e.g., Tabs, Plain, etc.).
988    #[props(default)]
989    pub variant: Variant,
990
991    /// Optional list of custom buttons displayed in the top bar.
992    #[props(default)]
993    pub custom_buttons: Vec<Element>,
994
995    /// CSS class applied to the outermost container.
996    ///
997    /// Defaults to:
998    /// `"rounded-lg border shadow-lg overflow-hidden bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700"`
999    #[props(
1000        default = "rounded-lg border shadow-lg overflow-hidden bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700"
1001    )]
1002    pub class: &'static str,
1003
1004    /// CSS class applied to the browser frame.
1005    #[props(default)]
1006    pub frame_class: &'static str,
1007
1008    /// Inline style string applied to the outermost container.
1009    #[props(default)]
1010    pub style: &'static str,
1011
1012    /// HTML id attribute for the browser container.
1013    #[props(default)]
1014    pub id: &'static str,
1015
1016    /// ARIA label for accessibility.
1017    ///
1018    /// Defaults to `"Browser window"`.
1019    #[props(default = "Browser window")]
1020    pub aria_label: &'static str,
1021
1022    /// ARIA description for additional accessibility context.
1023    #[props(default)]
1024    pub aria_describedby: &'static str,
1025
1026    /// Additional CSS class for the address bar container.
1027    #[props(default)]
1028    pub container_class: &'static str,
1029
1030    /// Additional CSS class for the input element in the address bar.
1031    ///
1032    /// Defaults to `"text-black dark:text-white"`.
1033    #[props(default = "text-black dark:text-white")]
1034    pub input_class: &'static str,
1035
1036    /// Inline style for the refresh button inside the address bar.
1037    #[props(
1038        default = "position: absolute; top: 50%; right: 8px; transform: translateY(-50%); padding: 4px; background: none; border: none; box-shadow: none; outline: none; cursor: pointer;"
1039    )]
1040    pub refresh_button_style: &'static str,
1041
1042    /// ARIA label for the refresh button.
1043    ///
1044    /// Defaults to `"Refresh"`.
1045    #[props(default = "Refresh")]
1046    pub refresh_button_aria_label: &'static str,
1047
1048    /// Shared inline style for all icon buttons (close, minimize, maximize).
1049    #[props(
1050        default = "padding: 4px; cursor: pointer; background: none; border: none; box-shadow: none; outline: none;"
1051    )]
1052    pub icon_button_style: &'static str,
1053
1054    /// Inline style for the wrapper around the address bar.
1055    #[props(default = "flex: 1; display: flex; justify-content: center; padding-right: 8px;")]
1056    pub address_wrapper_base_style: &'static str,
1057
1058    /// Inline style for the header container (holds address bar and controls).
1059    #[props(default = "display: flex; align-items: center; position: relative;")]
1060    pub header_base_style: &'static str,
1061
1062    // Close button props
1063    #[props(default)]
1064    pub on_close_mouse_over: EventHandler<()>,
1065    #[props(default)]
1066    pub on_close_mouse_out: EventHandler<()>,
1067    #[props(default)]
1068    pub on_close_focus: EventHandler<FocusEvent>,
1069    #[props(default)]
1070    pub on_close_blur: EventHandler<FocusEvent>,
1071    #[props(default)]
1072    pub close_class: &'static str,
1073    #[props(default)]
1074    pub close_svg_class: &'static str,
1075    #[props(default)]
1076    pub close_path_class: &'static str,
1077    #[props(default = "button")]
1078    pub close_button_type: &'static str,
1079    #[props(default)]
1080    pub close_aria_label: &'static str,
1081    #[props(default)]
1082    pub close_title: &'static str,
1083    #[props(default = "0")]
1084    pub close_tabindex: &'static str,
1085
1086    // Minimize button props
1087    #[props(default)]
1088    pub on_minimize_mouse_over: EventHandler<()>,
1089    #[props(default)]
1090    pub on_minimize_mouse_out: EventHandler<()>,
1091    #[props(default)]
1092    pub on_minimize_focus: EventHandler<FocusEvent>,
1093    #[props(default)]
1094    pub on_minimize_blur: EventHandler<FocusEvent>,
1095    #[props(default)]
1096    pub minimize_class: &'static str,
1097    #[props(default)]
1098    pub minimize_svg_class: &'static str,
1099    #[props(default)]
1100    pub minimize_path_class: &'static str,
1101    #[props(default = "button")]
1102    pub minimize_button_type: &'static str,
1103    #[props(default)]
1104    pub minimize_aria_label: &'static str,
1105    #[props(default)]
1106    pub minimize_title: &'static str,
1107    #[props(default = "0")]
1108    pub minimize_tabindex: &'static str,
1109
1110    // Maximize button props
1111    #[props(default)]
1112    pub on_maximize_mouse_over: EventHandler<()>,
1113    #[props(default)]
1114    pub on_maximize_mouse_out: EventHandler<()>,
1115    #[props(default)]
1116    pub on_maximize_focus: EventHandler<FocusEvent>,
1117    #[props(default)]
1118    pub on_maximize_blur: EventHandler<FocusEvent>,
1119    #[props(default)]
1120    pub maximize_class: &'static str,
1121    #[props(default)]
1122    pub maximize_svg_class: &'static str,
1123    #[props(default)]
1124    pub maximize_path_class: &'static str,
1125    #[props(default = "button")]
1126    pub maximize_button_type: &'static str,
1127    #[props(default)]
1128    pub maximize_aria_label: &'static str,
1129    #[props(default)]
1130    pub maximize_title: &'static str,
1131    #[props(default = "0")]
1132    pub maximize_tabindex: &'static str,
1133
1134    // Share button props
1135    #[props(default)]
1136    pub share_button_style: &'static str,
1137    #[props(default)]
1138    pub share_onclick: EventHandler<()>,
1139    #[props(default)]
1140    pub share_onmouseover: EventHandler<()>,
1141    #[props(default)]
1142    pub share_onmouseout: EventHandler<()>,
1143    #[props(default)]
1144    pub share_onfocus: EventHandler<FocusEvent>,
1145    #[props(default)]
1146    pub share_onblur: EventHandler<FocusEvent>,
1147    #[props(default)]
1148    pub share_tabindex: &'static str,
1149
1150    // Tabs button props
1151    #[props(default)]
1152    pub tabs_button_style: &'static str,
1153    #[props(default)]
1154    pub tabs_onclick: EventHandler<()>,
1155    #[props(default)]
1156    pub tabs_onmouseover: EventHandler<()>,
1157    #[props(default)]
1158    pub tabs_onmouseout: EventHandler<()>,
1159    #[props(default)]
1160    pub tabs_onfocus: EventHandler<FocusEvent>,
1161    #[props(default)]
1162    pub tabs_onblur: EventHandler<FocusEvent>,
1163    #[props(default)]
1164    pub tabs_tabindex: &'static str,
1165
1166    // More button props
1167    #[props(default)]
1168    pub more_button_style: &'static str,
1169    #[props(default)]
1170    pub more_onclick: EventHandler<()>,
1171    #[props(default)]
1172    pub more_onmouseover: EventHandler<()>,
1173    #[props(default)]
1174    pub more_onmouseout: EventHandler<()>,
1175    #[props(default)]
1176    pub more_onfocus: EventHandler<FocusEvent>,
1177    #[props(default)]
1178    pub more_onblur: EventHandler<FocusEvent>,
1179    #[props(default)]
1180    pub more_tabindex: &'static str,
1181}
1182
1183/// BrowserFrame Component
1184///
1185/// A Dioxus component that emulates a browser window, complete with customizable controls (close, minimize, maximize),
1186/// an address bar, and optional custom buttons. It wraps its child components in a browser-like interface and provides
1187/// various hooks for interaction events such as focus, hover, and clicks.
1188///
1189/// # Features
1190/// - Configurable address bar and controls (show/hide, read-only).
1191/// - Fully customizable styling and classes for different parts of the frame.
1192/// - Emits callbacks for URL changes and control interactions (close, minimize, maximize).
1193/// - Supports additional custom buttons and slots for user-defined functionality.
1194/// - Keyboard navigation support (Escape to close).
1195///
1196/// # Examples
1197///
1198/// ## Basic Usage
1199/// ```rust
1200/// use dioxus::prelude::*;
1201/// use browser_rs::dioxus::BrowserFrame;
1202///
1203/// fn app() -> Element {
1204///     let on_close = Callback::new(|_| log::info!("Browser closed"));
1205///
1206///     rsx! {
1207///         BrowserFrame {
1208///             url: "https://opensass.org",
1209///             on_close: on_close,
1210///             children: rsx! {
1211///                 p { "Your embedded content here." }
1212///             }
1213///         }
1214///     }
1215/// }
1216/// ```
1217///
1218/// ## With Custom Buttons
1219/// ```rust
1220/// use dioxus::prelude::*;
1221/// use browser_rs::dioxus::BrowserFrame;
1222///
1223/// fn app() -> Element {
1224///     let custom_button = rsx! {
1225///         button { "Custom Button" }
1226///     };
1227///
1228///     rsx! {
1229///         BrowserFrame {
1230///             url: "https://opensass.org",
1231///             custom_buttons: vec![custom_button],
1232///             children: rsx! {
1233///                 p { "Custom button in the header!" }
1234///             }
1235///         }
1236///     }
1237/// }
1238/// ```
1239///
1240/// ## Styling and Class Customization
1241/// ```rust
1242/// use dioxus::prelude::*;
1243/// use browser_rs::dioxus::BrowserFrame;
1244///
1245/// fn app() -> Element {
1246///     rsx! {
1247///         BrowserFrame {
1248///             url: "https://opensass.org",
1249///             class: "rounded-xl shadow-xl",
1250///             input_class: "bg-gray-200 text-gray-900",
1251///             container_class: "flex-1 mx-4",
1252///             children: rsx! {
1253///                 p { "Styled browser frame!" }
1254///             }
1255///         }
1256///     }
1257/// }
1258/// ```
1259///
1260/// # Behavior
1261/// - The `BrowserFrame` uses a `BrowserHeader` subcomponent for controls and an address bar,
1262///   and a `BrowserContent` subcomponent for rendering child content.
1263/// - The `on_url_change` callback is called when the address bar's URL changes.
1264/// - Control buttons (close, minimize, maximize) emit their respective callbacks when interacted with.
1265/// - Keyboard navigation is enabled: Escape key triggers the `on_close` callback.
1266///
1267/// # Notes
1268/// - Supports both light and dark themes through provided classes and styles.
1269/// - Default styling can be customized via `class`, `style`, and other related props.
1270/// - Accessibility attributes (`aria-*`) are provided.
1271#[component]
1272pub fn BrowserFrame(props: BrowserFrameProps) -> Element {
1273    let on_close = props.on_close;
1274
1275    let mut container_ref = use_keyboard(KeyboardNavigationOptions {
1276        on_escape: Some(EventHandler::new(move |_| {
1277            on_close.call(());
1278        })),
1279        on_enter: None,
1280        trap_focus: false,
1281    });
1282
1283    let size_style = props.size.to_style();
1284    let combined_style = format!("{} {}", size_style, props.style);
1285
1286    rsx! {
1287        article {
1288            id: "{props.id}",
1289            class: "{props.class}",
1290            style: "{combined_style}",
1291            role: "application",
1292            aria_label: "{props.aria_label}",
1293            aria_describedby: "{props.aria_describedby}",
1294            tabindex: "-1",
1295            onmounted: move |cx| container_ref.set(Some(cx.data())),
1296
1297            BrowserHeader {
1298                url: props.url,
1299                placeholder: props.placeholder,
1300                on_url_change: props.on_url_change,
1301                on_close: props.on_close,
1302                on_minimize: props.on_minimize,
1303                on_maximize: props.on_maximize,
1304                show_controls: props.show_controls,
1305                show_address_bar: props.show_address_bar,
1306                read_only: props.read_only,
1307                variant: props.variant,
1308                size: props.size,
1309                custom_buttons: props.custom_buttons,
1310                class: props.frame_class,
1311                container_class: props.container_class,
1312                input_class: props.input_class,
1313                refresh_button_style: props.refresh_button_style,
1314                refresh_button_aria_label: props.refresh_button_aria_label,
1315                icon_button_style: props.icon_button_style,
1316                address_wrapper_base_style: props.address_wrapper_base_style,
1317                header_base_style: props.header_base_style,
1318                on_close_mouse_over: props.on_close_mouse_over,
1319                on_close_mouse_out: props.on_close_mouse_out,
1320                on_close_focus: props.on_close_focus,
1321                on_close_blur: props.on_close_blur,
1322                close_class: props.close_class,
1323                close_svg_class: props.close_svg_class,
1324                close_path_class: props.close_path_class,
1325                close_button_type: props.close_button_type,
1326                close_aria_label: props.close_aria_label,
1327                close_title: props.close_title,
1328                close_tabindex: props.close_tabindex,
1329                on_minimize_mouse_over: props.on_minimize_mouse_over,
1330                on_minimize_mouse_out: props.on_minimize_mouse_out,
1331                on_minimize_focus: props.on_minimize_focus,
1332                on_minimize_blur: props.on_minimize_blur,
1333                minimize_class: props.minimize_class,
1334                minimize_svg_class: props.minimize_svg_class,
1335                minimize_path_class: props.minimize_path_class,
1336                minimize_button_type: props.minimize_button_type,
1337                minimize_aria_label: props.minimize_aria_label,
1338                minimize_title: props.minimize_title,
1339                minimize_tabindex: props.minimize_tabindex,
1340                on_maximize_mouse_over: props.on_maximize_mouse_over,
1341                on_maximize_mouse_out: props.on_maximize_mouse_out,
1342                on_maximize_focus: props.on_maximize_focus,
1343                on_maximize_blur: props.on_maximize_blur,
1344                maximize_class: props.maximize_class,
1345                maximize_svg_class: props.maximize_svg_class,
1346                maximize_path_class: props.maximize_path_class,
1347                maximize_button_type: props.maximize_button_type,
1348                maximize_aria_label: props.maximize_aria_label,
1349                maximize_title: props.maximize_title,
1350                maximize_tabindex: props.maximize_tabindex,
1351                share_button_style: props.share_button_style,
1352                share_onclick: props.share_onclick,
1353                share_onmouseover: props.share_onmouseover,
1354                share_onmouseout: props.share_onmouseout,
1355                share_onfocus: props.share_onfocus,
1356                share_onblur: props.share_onblur,
1357                share_tabindex: props.share_tabindex,
1358                tabs_button_style: props.tabs_button_style,
1359                tabs_onclick: props.tabs_onclick,
1360                tabs_onmouseover: props.tabs_onmouseover,
1361                tabs_onmouseout: props.tabs_onmouseout,
1362                tabs_onfocus: props.tabs_onfocus,
1363                tabs_onblur: props.tabs_onblur,
1364                tabs_tabindex: props.tabs_tabindex,
1365                more_button_style: props.more_button_style,
1366                more_onclick: props.more_onclick,
1367                more_onmouseover: props.more_onmouseover,
1368                more_onmouseout: props.more_onmouseout,
1369                more_onfocus: props.more_onfocus,
1370                more_onblur: props.more_onblur,
1371                more_tabindex: props.more_tabindex,
1372            }
1373            BrowserContent {
1374                aria_describedby: props.aria_describedby,
1375                {props.children}
1376            }
1377        }
1378    }
1379}