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
109                    .set_text_content(Some(&format!("Navigating to {}", input_value.read())));
110                document.body().unwrap().append_child(&live_region).unwrap();
111
112                let clone = live_region.clone();
113                Timeout::new(1000, move || {
114                    let _ = document.body().unwrap().remove_child(&clone);
115                })
116                .forget();
117            }
118        }
119    };
120
121    rsx! {
122        div {
123            class: "{props.container_class} {props.class}",
124            style: "{props.style}",
125            label {
126                r#for: "{props.input_id}",
127                class: "sr-only",
128                "{props.label}"
129            }
130            input {
131                id: "{props.input_id}",
132                r#type: "text",
133                class: "{props.input_class}",
134                style: "{props.input_style}",
135                value: "{input_value}",
136                oninput: on_input_change,
137                onkeydown: on_key_down,
138                onfocus: move |_| is_focused.set(true),
139                onblur: move |_| is_focused.set(false),
140                placeholder: "{props.placeholder}",
141                readonly: props.read_only,
142                aria_describedby: "{props.describedby}",
143                autocomplete: "url",
144                spellcheck: "false",
145                onmounted: move |cx| input_ref.set(Some(cx.data())),
146            }
147            button {
148                style: "{props.refresh_button_style}",
149                aria_label: "{props.refresh_button_aria_label}",
150                onclick: move |_| {
151                    let _ = window().unwrap().location().reload();
152                },
153                svg {
154                    width: "11",
155                    height: "13",
156                    view_box: "0 0 11 13",
157                    fill: "none",
158                    xmlns: "http://www.w3.org/2000/svg",
159                    path {
160                        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",
161                        stroke: "#767676",
162                        stroke_linecap: "round",
163                        stroke_linejoin: "round"
164                    }
165                }
166            }
167        }
168    }
169}
170
171#[derive(Props, PartialEq, Clone)]
172pub struct ControlButtonProps {
173    pub r#type: ButtonType,
174    #[props(default)]
175    pub on_click: EventHandler<()>,
176    #[props(default)]
177    pub on_mouse_over: EventHandler<()>,
178    #[props(default)]
179    pub on_mouse_out: EventHandler<()>,
180    #[props(default)]
181    pub on_focus: EventHandler<FocusEvent>,
182    #[props(default)]
183    pub on_blur: EventHandler<FocusEvent>,
184    #[props(
185        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;"
186    )]
187    pub style: &'static str,
188    #[props(default)]
189    pub class: &'static str,
190    #[props(default)]
191    pub svg_class: &'static str,
192    #[props(default)]
193    pub path_class: &'static str,
194    #[props(default = "button")]
195    pub button_type: &'static str,
196    #[props(default)]
197    pub aria_label: &'static str,
198    #[props(default)]
199    pub title: &'static str,
200    #[props(default = "0")]
201    pub tabindex: &'static str,
202}
203
204#[component]
205pub fn ControlButton(props: ControlButtonProps) -> Element {
206    let (fill, stroke) = match props.r#type {
207        ButtonType::Close => ("#FF5F57", "#E14640"),
208        ButtonType::Minimize => ("#FFBD2E", "#DFA123"),
209        ButtonType::Maximize => ("#28CA42", "#1DAD2C"),
210    };
211
212    let aria_label = if props.aria_label.is_empty() {
213        props.r#type.default_aria_label()
214    } else {
215        props.aria_label
216    };
217
218    let title = if props.title.is_empty() {
219        props.r#type.default_title()
220    } else {
221        props.title
222    };
223
224    rsx! {
225        button {
226            r#type: "{props.button_type}",
227            class: "{props.class}",
228            style: "{props.style}",
229            aria_label: "{aria_label}",
230            title: "{title}",
231            tabindex: "{props.tabindex}",
232            onclick: move |_| props.on_click.call(()),
233            onmouseover: move |_| props.on_mouse_over.call(()),
234            onmouseout: move |_| props.on_mouse_out.call(()),
235            onfocus: props.on_focus,
236            onblur: props.on_blur,
237
238            svg {
239                class: "{props.svg_class}",
240                width: "12",
241                height: "12",
242                view_box: "0 0 12 12",
243                fill: "none",
244                xmlns: "http://www.w3.org/2000/svg",
245                path {
246                    class: "{props.path_class}",
247                    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",
248                    fill: "{fill}",
249                    stroke: "{stroke}"
250                }
251            }
252        }
253    }
254}
255
256#[derive(PartialEq, Props, Clone)]
257pub struct BrowserControlsProps {
258    #[props(default)]
259    pub show_controls: bool,
260    #[props(default)]
261    pub class: &'static str,
262    #[props(default = "display: flex; align-items: center; background: none; padding-left: 10px;")]
263    pub style: &'static str,
264
265    #[props(default)]
266    pub on_close: EventHandler<()>,
267    #[props(default)]
268    pub on_close_mouse_over: EventHandler<()>,
269    #[props(default)]
270    pub on_close_mouse_out: EventHandler<()>,
271    #[props(default)]
272    pub on_close_focus: EventHandler<FocusEvent>,
273    #[props(default)]
274    pub on_close_blur: EventHandler<FocusEvent>,
275    #[props(default)]
276    pub close_class: &'static str,
277    #[props(default)]
278    pub close_svg_class: &'static str,
279    #[props(default)]
280    pub close_path_class: &'static str,
281    #[props(default = "button")]
282    pub close_button_type: &'static str,
283    #[props(default)]
284    pub close_aria_label: &'static str,
285    #[props(default)]
286    pub close_title: &'static str,
287    #[props(default = "0")]
288    pub close_tabindex: &'static str,
289
290    #[props(default)]
291    pub on_minimize: EventHandler<()>,
292    #[props(default)]
293    pub on_minimize_mouse_over: EventHandler<()>,
294    #[props(default)]
295    pub on_minimize_mouse_out: EventHandler<()>,
296    #[props(default)]
297    pub on_minimize_focus: EventHandler<FocusEvent>,
298    #[props(default)]
299    pub on_minimize_blur: EventHandler<FocusEvent>,
300    #[props(default)]
301    pub minimize_class: &'static str,
302    #[props(default)]
303    pub minimize_svg_class: &'static str,
304    #[props(default)]
305    pub minimize_path_class: &'static str,
306    #[props(default = "button")]
307    pub minimize_button_type: &'static str,
308    #[props(default)]
309    pub minimize_aria_label: &'static str,
310    #[props(default)]
311    pub minimize_title: &'static str,
312    #[props(default = "0")]
313    pub minimize_tabindex: &'static str,
314
315    #[props(default)]
316    pub on_maximize: EventHandler<()>,
317    #[props(default)]
318    pub on_maximize_mouse_over: EventHandler<()>,
319    #[props(default)]
320    pub on_maximize_mouse_out: EventHandler<()>,
321    #[props(default)]
322    pub on_maximize_focus: EventHandler<FocusEvent>,
323    #[props(default)]
324    pub on_maximize_blur: EventHandler<FocusEvent>,
325    #[props(default)]
326    pub maximize_class: &'static str,
327    #[props(default)]
328    pub maximize_svg_class: &'static str,
329    #[props(default)]
330    pub maximize_path_class: &'static str,
331    #[props(default = "button")]
332    pub maximize_button_type: &'static str,
333    #[props(default)]
334    pub maximize_aria_label: &'static str,
335    #[props(default)]
336    pub maximize_title: &'static str,
337    #[props(default = "0")]
338    pub maximize_tabindex: &'static str,
339}
340
341#[component]
342pub fn BrowserControls(props: BrowserControlsProps) -> Element {
343    if !props.show_controls {
344        return rsx! {};
345    }
346
347    rsx! {
348        nav {
349            class: "{props.class}",
350            style: "{props.style}",
351            role: "toolbar",
352            aria_label: "Browser window controls",
353            ControlButton {
354                r#type: ButtonType::Close,
355                on_click: props.on_close,
356                on_mouse_over: props.on_close_mouse_over,
357                on_mouse_out: props.on_close_mouse_out,
358                on_focus: props.on_close_focus,
359                on_blur: props.on_close_blur,
360                class: props.close_class,
361                svg_class: props.close_svg_class,
362                path_class: props.close_path_class,
363                button_type: props.close_button_type,
364                aria_label: props.close_aria_label,
365                title: props.close_title,
366                tabindex: props.close_tabindex,
367            }
368            ControlButton {
369                r#type: ButtonType::Minimize,
370                on_click: props.on_minimize,
371                on_mouse_over: props.on_minimize_mouse_over,
372                on_mouse_out: props.on_minimize_mouse_out,
373                on_focus: props.on_minimize_focus,
374                on_blur: props.on_minimize_blur,
375                class: props.minimize_class,
376                svg_class: props.minimize_svg_class,
377                path_class: props.minimize_path_class,
378                button_type: props.minimize_button_type,
379                aria_label: props.minimize_aria_label,
380                title: props.minimize_title,
381                tabindex: props.minimize_tabindex,
382            }
383            ControlButton {
384                r#type: ButtonType::Maximize,
385                on_click: props.on_maximize,
386                on_mouse_over: props.on_maximize_mouse_over,
387                on_mouse_out: props.on_maximize_mouse_out,
388                on_focus: props.on_maximize_focus,
389                on_blur: props.on_maximize_blur,
390                class: props.maximize_class,
391                svg_class: props.maximize_svg_class,
392                path_class: props.maximize_path_class,
393                button_type: props.maximize_button_type,
394                aria_label: props.maximize_aria_label,
395                title: props.maximize_title,
396                tabindex: props.maximize_tabindex,
397            }
398        }
399    }
400}
401
402#[derive(PartialEq, Props, Clone)]
403pub struct BrowserHeaderProps {
404    #[props(default)]
405    pub url: String,
406    #[props(default)]
407    pub placeholder: &'static str,
408    #[props(default)]
409    pub on_url_change: Option<EventHandler<FormEvent>>,
410    #[props(default = true)]
411    pub show_controls: bool,
412    #[props(default = true)]
413    pub show_address_bar: bool,
414    #[props(default = false)]
415    pub read_only: bool,
416    #[props(default)]
417    pub variant: Variant,
418    #[props(default)]
419    pub size: Size,
420    #[props(default)]
421    pub custom_buttons: Vec<Element>,
422    #[props(default)]
423    pub class: &'static str,
424
425    #[props(default)]
426    pub container_class: &'static str,
427    #[props(default = "text-black dark:text-white")]
428    pub input_class: &'static str,
429    #[props(default)]
430    pub refresh_button_style: &'static str,
431    #[props(default = "Refresh")]
432    pub refresh_button_aria_label: &'static str,
433
434    #[props(
435        default = "padding: 4px; cursor: pointer; background: none; border: none; box-shadow: none; outline: none;"
436    )]
437    pub icon_button_style: &'static str,
438
439    #[props(default = "flex: 1; display: flex; justify-content: center; padding-right: 8px;")]
440    pub address_wrapper_base_style: &'static str,
441
442    #[props(default = "display: flex; align-items: center; position: relative;")]
443    pub header_base_style: &'static str,
444
445    #[props(default)]
446    pub on_close: EventHandler<()>,
447    #[props(default)]
448    pub on_close_mouse_over: EventHandler<()>,
449    #[props(default)]
450    pub on_close_mouse_out: EventHandler<()>,
451    #[props(default)]
452    pub on_close_focus: EventHandler<FocusEvent>,
453    #[props(default)]
454    pub on_close_blur: EventHandler<FocusEvent>,
455    #[props(default)]
456    pub close_class: &'static str,
457    #[props(default)]
458    pub close_svg_class: &'static str,
459    #[props(default)]
460    pub close_path_class: &'static str,
461    #[props(default = "button")]
462    pub close_button_type: &'static str,
463    #[props(default)]
464    pub close_aria_label: &'static str,
465    #[props(default)]
466    pub close_title: &'static str,
467    #[props(default = "0")]
468    pub close_tabindex: &'static str,
469
470    #[props(default)]
471    pub on_minimize: EventHandler<()>,
472    #[props(default)]
473    pub on_minimize_mouse_over: EventHandler<()>,
474    #[props(default)]
475    pub on_minimize_mouse_out: EventHandler<()>,
476    #[props(default)]
477    pub on_minimize_focus: EventHandler<FocusEvent>,
478    #[props(default)]
479    pub on_minimize_blur: EventHandler<FocusEvent>,
480    #[props(default)]
481    pub minimize_class: &'static str,
482    #[props(default)]
483    pub minimize_svg_class: &'static str,
484    #[props(default)]
485    pub minimize_path_class: &'static str,
486    #[props(default = "button")]
487    pub minimize_button_type: &'static str,
488    #[props(default)]
489    pub minimize_aria_label: &'static str,
490    #[props(default)]
491    pub minimize_title: &'static str,
492    #[props(default = "0")]
493    pub minimize_tabindex: &'static str,
494
495    #[props(default)]
496    pub on_maximize: EventHandler<()>,
497    #[props(default)]
498    pub on_maximize_mouse_over: EventHandler<()>,
499    #[props(default)]
500    pub on_maximize_mouse_out: EventHandler<()>,
501    #[props(default)]
502    pub on_maximize_focus: EventHandler<FocusEvent>,
503    #[props(default)]
504    pub on_maximize_blur: EventHandler<FocusEvent>,
505    #[props(default)]
506    pub maximize_class: &'static str,
507    #[props(default)]
508    pub maximize_svg_class: &'static str,
509    #[props(default)]
510    pub maximize_path_class: &'static str,
511    #[props(default = "button")]
512    pub maximize_button_type: &'static str,
513    #[props(default)]
514    pub maximize_aria_label: &'static str,
515    #[props(default)]
516    pub maximize_title: &'static str,
517    #[props(default = "0")]
518    pub maximize_tabindex: &'static str,
519
520    #[props(default)]
521    pub share_button_style: &'static str,
522    #[props(default)]
523    pub share_onclick: EventHandler<()>,
524    #[props(default)]
525    pub share_onmouseover: EventHandler<()>,
526    #[props(default)]
527    pub share_onmouseout: EventHandler<()>,
528    #[props(default)]
529    pub share_onfocus: EventHandler<FocusEvent>,
530    #[props(default)]
531    pub share_onblur: EventHandler<FocusEvent>,
532    #[props(default)]
533    pub share_tabindex: &'static str,
534
535    #[props(default)]
536    pub tabs_button_style: &'static str,
537    #[props(default)]
538    pub tabs_onclick: EventHandler<()>,
539    #[props(default)]
540    pub tabs_onmouseover: EventHandler<()>,
541    #[props(default)]
542    pub tabs_onmouseout: EventHandler<()>,
543    #[props(default)]
544    pub tabs_onfocus: EventHandler<FocusEvent>,
545    #[props(default)]
546    pub tabs_onblur: EventHandler<FocusEvent>,
547    #[props(default)]
548    pub tabs_tabindex: &'static str,
549
550    #[props(default)]
551    pub more_button_style: &'static str,
552    #[props(default)]
553    pub more_onclick: EventHandler<()>,
554    #[props(default)]
555    pub more_onmouseover: EventHandler<()>,
556    #[props(default)]
557    pub more_onmouseout: EventHandler<()>,
558    #[props(default)]
559    pub more_onfocus: EventHandler<FocusEvent>,
560    #[props(default)]
561    pub more_onblur: EventHandler<FocusEvent>,
562    #[props(default)]
563    pub more_tabindex: &'static str,
564}
565
566#[component]
567pub fn BrowserHeader(props: BrowserHeaderProps) -> Element {
568    let is_ios = props.variant == Variant::Ios;
569    let is_tabs = props.variant == Variant::Tabs;
570
571    let base_style = {
572        let padding = match props.size {
573            Size::Small => "4px 6px",
574            Size::Large => "10px 16px",
575            _ => "6px 12px",
576        };
577        let height = match (props.variant.clone(), props.size.clone()) {
578            (Variant::Tabs, _) => "40px",
579            (Variant::Ios, _) => "56px",
580            (_, Size::Large) => "60px",
581            (_, Size::Small) => "38px",
582            _ => "48px",
583        };
584        let border_radius = if is_tabs {
585            "6px"
586        } else if props.variant == Variant::Default {
587            "8px 8px 0 0"
588        } else {
589            "0"
590        };
591        let border = if is_tabs { "1px solid #d1d5db" } else { "none" };
592        let box_shadow = if props.variant == Variant::Default {
593            "0 2px 6px rgba(0,0,0,0.1)"
594        } else {
595            "none"
596        };
597
598        format!(
599            "{} justify-content: {}; padding: {}; height: {}; border-radius: {}; border: {}; box-shadow: {};",
600            props.header_base_style,
601            if is_ios {
602                "space-between"
603            } else {
604                "flex-start"
605            },
606            padding,
607            height,
608            border_radius,
609            border,
610            box_shadow
611        )
612    };
613
614    let address_wrapper_style = format!(
615        "{} padding-left: {};",
616        props.address_wrapper_base_style,
617        if props.show_controls { "8px" } else { "0" }
618    );
619
620    let share_onclick = move |_| props.share_onclick.call(());
621    let share_onmouseover = move |_| props.share_onmouseover.call(());
622    let share_onmouseout = move |_| props.share_onmouseout.call(());
623
624    let tabs_onclick = move |_| props.tabs_onclick.call(());
625    let tabs_onmouseover = move |_| props.tabs_onmouseover.call(());
626    let tabs_onmouseout = move |_| props.tabs_onmouseout.call(());
627
628    let more_onclick = move |_| props.more_onclick.call(());
629    let more_onmouseover = move |_| props.more_onmouseover.call(());
630    let more_onmouseout = move |_| props.more_onmouseout.call(());
631
632    rsx! {
633        header {
634            style: "{base_style}",
635            class: "{props.class}",
636            "aria-label": "Browser window header",
637
638            div {
639                style: "display: flex; align-items: center; gap: 6px;",
640                if props.show_controls {
641                    BrowserControls {
642                        on_close: props.on_close,
643                        on_minimize: props.on_minimize,
644                        on_maximize: props.on_maximize,
645                        show_controls: props.show_controls,
646                        on_close_mouse_over: props.on_close_mouse_over,
647                        on_close_mouse_out: props.on_close_mouse_out,
648                        on_close_focus: props.on_close_focus,
649                        on_close_blur: props.on_close_blur,
650                        close_class: props.close_class,
651                        close_svg_class: props.close_svg_class,
652                        close_path_class: props.close_path_class,
653                        close_button_type: props.close_button_type,
654                        close_aria_label: props.close_aria_label,
655                        close_title: props.close_title,
656                        close_tabindex: props.close_tabindex,
657                        on_minimize_mouse_over: props.on_minimize_mouse_over,
658                        on_minimize_mouse_out: props.on_minimize_mouse_out,
659                        on_minimize_focus: props.on_minimize_focus,
660                        on_minimize_blur: props.on_minimize_blur,
661                        minimize_class: props.minimize_class,
662                        minimize_svg_class: props.minimize_svg_class,
663                        minimize_path_class: props.minimize_path_class,
664                        minimize_button_type: props.minimize_button_type,
665                        minimize_aria_label: props.minimize_aria_label,
666                        minimize_title: props.minimize_title,
667                        minimize_tabindex: props.minimize_tabindex,
668                        on_maximize_mouse_over: props.on_maximize_mouse_over,
669                        on_maximize_mouse_out: props.on_maximize_mouse_out,
670                        on_maximize_focus: props.on_maximize_focus,
671                        on_maximize_blur: props.on_maximize_blur,
672                        maximize_class: props.maximize_class,
673                        maximize_svg_class: props.maximize_svg_class,
674                        maximize_path_class: props.maximize_path_class,
675                        maximize_button_type: props.maximize_button_type,
676                        maximize_aria_label: props.maximize_aria_label,
677                        maximize_title: props.maximize_title,
678                        maximize_tabindex: props.maximize_tabindex,
679                    }
680                    if !is_ios {
681                        button {
682                            style: "{props.icon_button_style}",
683                            "aria-label": "Sidebar",
684                            svg {
685                                width: "20",
686                                height: "15",
687                                view_box: "0 0 20 15",
688                                fill: "none",
689                                xmlns: "http://www.w3.org/2000/svg",
690                                path {
691                                    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",
692                                    fill: "#767676",
693                                }
694                            }
695                        }
696                        button {
697                            style: "{props.icon_button_style}",
698                            "aria-label": "Back",
699                            svg {
700                                width: "9",
701                                height: "16",
702                                view_box: "0 0 9 16",
703                                fill: "none",
704                                xmlns: "http://www.w3.org/2000/svg",
705                                path {
706                                    d: "M7.5 1.5L1 8L7.5 14.5",
707                                    stroke: "#737373",
708                                    stroke_width: "1.5",
709                                    stroke_linecap: "round",
710                                    stroke_linejoin: "round",
711                                }
712                            }
713                        }
714                        button {
715                            style: "{props.icon_button_style}",
716                            "aria-label": "Forward",
717                            svg {
718                                width: "9",
719                                height: "16",
720                                view_box: "0 0 9 16",
721                                fill: "none",
722                                xmlns: "http://www.w3.org/2000/svg",
723                                path {
724                                    d: "M1 14.5L7.5 8L1 1.5",
725                                    stroke: "#BFBFBF",
726                                    stroke_width: "1.5",
727                                    stroke_linecap: "round",
728                                    stroke_linejoin: "round",
729                                }
730                            }
731                        }
732                    }
733                }
734            }
735
736            if props.show_address_bar {
737                div {
738                    style: "{address_wrapper_style}",
739                    AddressBar {
740                        url: props.url,
741                        placeholder: props.placeholder,
742                        on_url_change: props.on_url_change.unwrap_or_default(),
743                        read_only: props.read_only,
744                        input_class: props.input_class,
745                        container_class: props.container_class,
746                        refresh_button_style: props.refresh_button_style,
747                        refresh_button_aria_label: props.refresh_button_aria_label,
748                    }
749                }
750            }
751
752            div {
753                style: "display: flex; align-items: center; gap: 6px; margin-left: auto;",
754                if props.show_controls {
755                    for btn in &props.custom_buttons {
756                        {btn}
757                    }
758                    button {
759                        style: "{props.icon_button_style}",
760                        onclick: share_onclick,
761                        onmouseover: share_onmouseover,
762                        onmouseout: share_onmouseout,
763                        onfocus: props.share_onfocus,
764                        onblur: props.share_onblur,
765                        "aria-label": "Share",
766                        title: "Share",
767                        tabindex: "{props.share_tabindex}",
768                        svg {
769                            width: "15",
770                            height: "19",
771                            view_box: "0 0 15 19",
772                            fill: "none",
773                            xmlns: "http://www.w3.org/2000/svg",
774                            path {
775                                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",
776                                fill: "#767676",
777                            }
778                        }
779                    }
780                    button {
781                        style: "{props.icon_button_style}",
782                        onclick: tabs_onclick,
783                        onmouseover: tabs_onmouseover,
784                        onmouseout: tabs_onmouseout,
785                        onfocus: props.tabs_onfocus,
786                        onblur: props.tabs_onblur,
787                        "aria-label": "Tabs",
788                        title: "Tabs",
789                        tabindex: "{props.tabs_tabindex}",
790                        svg {
791                            width: "15",
792                            height: "15",
793                            view_box: "0 0 15 15",
794                            fill: "none",
795                            xmlns: "http://www.w3.org/2000/svg",
796                            path {
797                                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",
798                                fill: "#767676",
799                            }
800                        }
801                    }
802                    button {
803                        style: "{props.icon_button_style}",
804                        onclick: more_onclick,
805                        onmouseover: more_onmouseover,
806                        onmouseout: more_onmouseout,
807                        onfocus: props.more_onfocus,
808                        onblur: props.more_onblur,
809                        "aria-label": "More options",
810                        title: "More options",
811                        tabindex: "{props.more_tabindex}",
812                        svg {
813                            width: "18",
814                            height: "19",
815                            view_box: "0 0 18 19",
816                            fill: "none",
817                            xmlns: "http://www.w3.org/2000/svg",
818                            path {
819                                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",
820                                fill: "#767676",
821                            }
822                        }
823                    }
824                }
825            }
826        }
827    }
828}
829
830#[derive(Clone, PartialEq, Props)]
831pub struct KeyboardNavigationOptions {
832    pub on_escape: Option<EventHandler<()>>,
833    pub on_enter: Option<EventHandler<()>>,
834    pub trap_focus: bool,
835}
836
837pub fn use_keyboard(options: KeyboardNavigationOptions) -> Signal<Option<Rc<MountedData>>> {
838    let container_ref: Signal<Option<Rc<MountedData>>> = use_signal(|| None);
839
840    {
841        let options = options.clone();
842
843        use_effect(move || {
844            let closure = Closure::<dyn Fn(web_sys::KeyboardEvent)>::wrap(Box::new(
845                move |event: web_sys::KeyboardEvent| {
846                    let key = event.key();
847                    let target = event.target();
848
849                    match key.as_str() {
850                        "Escape" => {
851                            if let Some(callback) = &options.on_escape {
852                                event.prevent_default();
853                                callback.call(());
854                            }
855                        }
856                        "Enter" => {
857                            if let Some(callback) = &options.on_enter {
858                                if let Some(_target_elem) = target
859                                    .clone()
860                                    .and_then(|t| t.dyn_into::<web_sys::Element>().ok())
861                                {
862                                    if let Some(container) = container_ref
863                                        .read()
864                                        .as_ref()
865                                        .and_then(|r| r.downcast::<web_sys::Element>())
866                                    {
867                                        if target.unwrap() == ***container {
868                                            event.prevent_default();
869                                            callback.call(());
870                                        }
871                                    }
872                                }
873                            }
874                        }
875                        "Tab" if options.trap_focus => {
876                            if let Some(container) = container_ref
877                                .read()
878                                .as_ref()
879                                .and_then(|r| r.downcast::<web_sys::Element>())
880                            {
881                                let selector = "button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])";
882                                let focusables = container.query_selector_all(selector).unwrap();
883
884                                let length = focusables.length();
885                                if length == 0 {
886                                    return;
887                                }
888
889                                let first = focusables
890                                    .item(0)
891                                    .and_then(|e| e.dyn_into::<web_sys::HtmlElement>().ok());
892                                let last = focusables
893                                    .item(length - 1)
894                                    .and_then(|e| e.dyn_into::<web_sys::HtmlElement>().ok());
895
896                                let document = web_sys::window().unwrap().document().unwrap();
897                                let active = document.active_element();
898
899                                if event.shift_key() {
900                                    if active == first.as_ref().map(|e| e.clone().into()) {
901                                        event.prevent_default();
902                                        if let Some(elem) = last {
903                                            elem.focus().ok();
904                                        }
905                                    }
906                                } else if active == last.as_ref().map(|e| e.clone().into()) {
907                                    event.prevent_default();
908                                    if let Some(elem) = first {
909                                        elem.focus().ok();
910                                    }
911                                }
912                            }
913                        }
914                        _ => {}
915                    }
916                },
917            )
918                as Box<dyn Fn(web_sys::KeyboardEvent)>);
919
920            web_sys::window()
921                .unwrap()
922                .add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref())
923                .unwrap();
924        });
925    }
926
927    container_ref
928}
929/// Properties for the `BrowserFrame` component.
930///
931/// This component simulates a web browser window with controls like
932/// close, minimize, maximize, and supports rich customization via props.
933#[derive(PartialEq, Props, Clone)]
934pub struct BrowserFrameProps {
935    /// Child elements rendered inside the browser frame body.
936    #[props(default)]
937    pub children: Element,
938
939    /// The URL displayed in the address bar and used in the iframe.
940    #[props(default)]
941    pub url: String,
942
943    /// Placeholder text shown in the address input field.
944    #[props(default)]
945    pub placeholder: &'static str,
946
947    /// Event handler for when the address bar URL changes.
948    #[props(default)]
949    pub on_url_change: Option<EventHandler<FormEvent>>,
950
951    /// Event handler triggered when the close button is clicked.
952    #[props(default)]
953    pub on_close: EventHandler<()>,
954
955    /// Event handler triggered when the minimize button is clicked.
956    #[props(default)]
957    pub on_minimize: EventHandler<()>,
958
959    /// Event handler triggered when the maximize button is clicked.
960    #[props(default)]
961    pub on_maximize: EventHandler<()>,
962
963    /// Whether to show the top-right control buttons (close, minimize, maximize).
964    ///
965    /// Defaults to `true`.
966    #[props(default = true)]
967    pub show_controls: bool,
968
969    /// Whether to show the address bar.
970    ///
971    /// Defaults to `true`.
972    #[props(default = true)]
973    pub show_address_bar: bool,
974
975    /// Whether the address bar is read-only.
976    ///
977    /// Defaults to `false`.
978    #[props(default = false)]
979    pub read_only: bool,
980
981    /// Size of the browser frame container.
982    #[props(default)]
983    pub size: Size,
984
985    /// Display variant for the frame (e.g., Tabs, Plain, etc.).
986    #[props(default)]
987    pub variant: Variant,
988
989    /// Optional list of custom buttons displayed in the top bar.
990    #[props(default)]
991    pub custom_buttons: Vec<Element>,
992
993    /// CSS class applied to the outermost container.
994    ///
995    /// Defaults to:
996    /// `"rounded-lg border shadow-lg overflow-hidden bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700"`
997    #[props(
998        default = "rounded-lg border shadow-lg overflow-hidden bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700"
999    )]
1000    pub class: &'static str,
1001
1002    /// CSS class applied to the browser frame.
1003    #[props(default)]
1004    pub frame_class: &'static str,
1005
1006    /// Inline style string applied to the outermost container.
1007    #[props(default)]
1008    pub style: &'static str,
1009
1010    /// HTML id attribute for the browser container.
1011    #[props(default)]
1012    pub id: &'static str,
1013
1014    /// ARIA label for accessibility.
1015    ///
1016    /// Defaults to `"Browser window"`.
1017    #[props(default = "Browser window")]
1018    pub aria_label: &'static str,
1019
1020    /// ARIA description for additional accessibility context.
1021    #[props(default)]
1022    pub aria_describedby: &'static str,
1023
1024    /// Additional CSS class for the address bar container.
1025    #[props(default)]
1026    pub container_class: &'static str,
1027
1028    /// Additional CSS class for the input element in the address bar.
1029    ///
1030    /// Defaults to `"text-black dark:text-white"`.
1031    #[props(default = "text-black dark:text-white")]
1032    pub input_class: &'static str,
1033
1034    /// Inline style for the refresh button inside the address bar.
1035    #[props(
1036        default = "position: absolute; top: 50%; right: 8px; transform: translateY(-50%); padding: 4px; background: none; border: none; box-shadow: none; outline: none; cursor: pointer;"
1037    )]
1038    pub refresh_button_style: &'static str,
1039
1040    /// ARIA label for the refresh button.
1041    ///
1042    /// Defaults to `"Refresh"`.
1043    #[props(default = "Refresh")]
1044    pub refresh_button_aria_label: &'static str,
1045
1046    /// Shared inline style for all icon buttons (close, minimize, maximize).
1047    #[props(
1048        default = "padding: 4px; cursor: pointer; background: none; border: none; box-shadow: none; outline: none;"
1049    )]
1050    pub icon_button_style: &'static str,
1051
1052    /// Inline style for the wrapper around the address bar.
1053    #[props(default = "flex: 1; display: flex; justify-content: center; padding-right: 8px;")]
1054    pub address_wrapper_base_style: &'static str,
1055
1056    /// Inline style for the header container (holds address bar and controls).
1057    #[props(default = "display: flex; align-items: center; position: relative;")]
1058    pub header_base_style: &'static str,
1059
1060    // Close button props
1061    #[props(default)]
1062    pub on_close_mouse_over: EventHandler<()>,
1063    #[props(default)]
1064    pub on_close_mouse_out: EventHandler<()>,
1065    #[props(default)]
1066    pub on_close_focus: EventHandler<FocusEvent>,
1067    #[props(default)]
1068    pub on_close_blur: EventHandler<FocusEvent>,
1069    #[props(default)]
1070    pub close_class: &'static str,
1071    #[props(default)]
1072    pub close_svg_class: &'static str,
1073    #[props(default)]
1074    pub close_path_class: &'static str,
1075    #[props(default = "button")]
1076    pub close_button_type: &'static str,
1077    #[props(default)]
1078    pub close_aria_label: &'static str,
1079    #[props(default)]
1080    pub close_title: &'static str,
1081    #[props(default = "0")]
1082    pub close_tabindex: &'static str,
1083
1084    // Minimize button props
1085    #[props(default)]
1086    pub on_minimize_mouse_over: EventHandler<()>,
1087    #[props(default)]
1088    pub on_minimize_mouse_out: EventHandler<()>,
1089    #[props(default)]
1090    pub on_minimize_focus: EventHandler<FocusEvent>,
1091    #[props(default)]
1092    pub on_minimize_blur: EventHandler<FocusEvent>,
1093    #[props(default)]
1094    pub minimize_class: &'static str,
1095    #[props(default)]
1096    pub minimize_svg_class: &'static str,
1097    #[props(default)]
1098    pub minimize_path_class: &'static str,
1099    #[props(default = "button")]
1100    pub minimize_button_type: &'static str,
1101    #[props(default)]
1102    pub minimize_aria_label: &'static str,
1103    #[props(default)]
1104    pub minimize_title: &'static str,
1105    #[props(default = "0")]
1106    pub minimize_tabindex: &'static str,
1107
1108    // Maximize button props
1109    #[props(default)]
1110    pub on_maximize_mouse_over: EventHandler<()>,
1111    #[props(default)]
1112    pub on_maximize_mouse_out: EventHandler<()>,
1113    #[props(default)]
1114    pub on_maximize_focus: EventHandler<FocusEvent>,
1115    #[props(default)]
1116    pub on_maximize_blur: EventHandler<FocusEvent>,
1117    #[props(default)]
1118    pub maximize_class: &'static str,
1119    #[props(default)]
1120    pub maximize_svg_class: &'static str,
1121    #[props(default)]
1122    pub maximize_path_class: &'static str,
1123    #[props(default = "button")]
1124    pub maximize_button_type: &'static str,
1125    #[props(default)]
1126    pub maximize_aria_label: &'static str,
1127    #[props(default)]
1128    pub maximize_title: &'static str,
1129    #[props(default = "0")]
1130    pub maximize_tabindex: &'static str,
1131
1132    // Share button props
1133    #[props(default)]
1134    pub share_button_style: &'static str,
1135    #[props(default)]
1136    pub share_onclick: EventHandler<()>,
1137    #[props(default)]
1138    pub share_onmouseover: EventHandler<()>,
1139    #[props(default)]
1140    pub share_onmouseout: EventHandler<()>,
1141    #[props(default)]
1142    pub share_onfocus: EventHandler<FocusEvent>,
1143    #[props(default)]
1144    pub share_onblur: EventHandler<FocusEvent>,
1145    #[props(default)]
1146    pub share_tabindex: &'static str,
1147
1148    // Tabs button props
1149    #[props(default)]
1150    pub tabs_button_style: &'static str,
1151    #[props(default)]
1152    pub tabs_onclick: EventHandler<()>,
1153    #[props(default)]
1154    pub tabs_onmouseover: EventHandler<()>,
1155    #[props(default)]
1156    pub tabs_onmouseout: EventHandler<()>,
1157    #[props(default)]
1158    pub tabs_onfocus: EventHandler<FocusEvent>,
1159    #[props(default)]
1160    pub tabs_onblur: EventHandler<FocusEvent>,
1161    #[props(default)]
1162    pub tabs_tabindex: &'static str,
1163
1164    // More button props
1165    #[props(default)]
1166    pub more_button_style: &'static str,
1167    #[props(default)]
1168    pub more_onclick: EventHandler<()>,
1169    #[props(default)]
1170    pub more_onmouseover: EventHandler<()>,
1171    #[props(default)]
1172    pub more_onmouseout: EventHandler<()>,
1173    #[props(default)]
1174    pub more_onfocus: EventHandler<FocusEvent>,
1175    #[props(default)]
1176    pub more_onblur: EventHandler<FocusEvent>,
1177    #[props(default)]
1178    pub more_tabindex: &'static str,
1179}
1180
1181/// BrowserFrame Component
1182///
1183/// A Dioxus component that emulates a browser window, complete with customizable controls (close, minimize, maximize),
1184/// an address bar, and optional custom buttons. It wraps its child components in a browser-like interface and provides
1185/// various hooks for interaction events such as focus, hover, and clicks.
1186///
1187/// # Features
1188/// - Configurable address bar and controls (show/hide, read-only).
1189/// - Fully customizable styling and classes for different parts of the frame.
1190/// - Emits callbacks for URL changes and control interactions (close, minimize, maximize).
1191/// - Supports additional custom buttons and slots for user-defined functionality.
1192/// - Keyboard navigation support (Escape to close).
1193///
1194/// # Examples
1195///
1196/// ## Basic Usage
1197/// ```rust
1198/// use dioxus::prelude::*;
1199/// use browser_rs::dioxus::BrowserFrame;
1200///
1201/// fn app() -> Element {
1202///     let on_close = Callback::new(|_| log::info!("Browser closed"));
1203///
1204///     rsx! {
1205///         BrowserFrame {
1206///             url: "https://opensass.org",
1207///             on_close: on_close,
1208///             children: rsx! {
1209///                 p { "Your embedded content here." }
1210///             }
1211///         }
1212///     }
1213/// }
1214/// ```
1215///
1216/// ## With Custom Buttons
1217/// ```rust
1218/// use dioxus::prelude::*;
1219/// use browser_rs::dioxus::BrowserFrame;
1220///
1221/// fn app() -> Element {
1222///     let custom_button = rsx! {
1223///         button { "Custom Button" }
1224///     };
1225///
1226///     rsx! {
1227///         BrowserFrame {
1228///             url: "https://opensass.org",
1229///             custom_buttons: vec![custom_button],
1230///             children: rsx! {
1231///                 p { "Custom button in the header!" }
1232///             }
1233///         }
1234///     }
1235/// }
1236/// ```
1237///
1238/// ## Styling and Class Customization
1239/// ```rust
1240/// use dioxus::prelude::*;
1241/// use browser_rs::dioxus::BrowserFrame;
1242///
1243/// fn app() -> Element {
1244///     rsx! {
1245///         BrowserFrame {
1246///             url: "https://opensass.org",
1247///             class: "rounded-xl shadow-xl",
1248///             input_class: "bg-gray-200 text-gray-900",
1249///             container_class: "flex-1 mx-4",
1250///             children: rsx! {
1251///                 p { "Styled browser frame!" }
1252///             }
1253///         }
1254///     }
1255/// }
1256/// ```
1257///
1258/// # Behavior
1259/// - The `BrowserFrame` uses a `BrowserHeader` subcomponent for controls and an address bar,
1260///   and a `BrowserContent` subcomponent for rendering child content.
1261/// - The `on_url_change` callback is called when the address bar's URL changes.
1262/// - Control buttons (close, minimize, maximize) emit their respective callbacks when interacted with.
1263/// - Keyboard navigation is enabled: Escape key triggers the `on_close` callback.
1264///
1265/// # Notes
1266/// - Supports both light and dark themes through provided classes and styles.
1267/// - Default styling can be customized via `class`, `style`, and other related props.
1268/// - Accessibility attributes (`aria-*`) are provided.
1269#[component]
1270pub fn BrowserFrame(props: BrowserFrameProps) -> Element {
1271    let on_close = props.on_close;
1272
1273    let mut container_ref = use_keyboard(KeyboardNavigationOptions {
1274        on_escape: Some(EventHandler::new(move |_| {
1275            on_close.call(());
1276        })),
1277        on_enter: None,
1278        trap_focus: false,
1279    });
1280
1281    let size_style = props.size.to_style();
1282    let combined_style = format!("{} {}", size_style, props.style);
1283
1284    rsx! {
1285        article {
1286            id: "{props.id}",
1287            class: "{props.class}",
1288            style: "{combined_style}",
1289            role: "application",
1290            aria_label: "{props.aria_label}",
1291            aria_describedby: "{props.aria_describedby}",
1292            tabindex: "-1",
1293            onmounted: move |cx| container_ref.set(Some(cx.data())),
1294
1295            BrowserHeader {
1296                url: props.url,
1297                placeholder: props.placeholder,
1298                on_url_change: props.on_url_change,
1299                on_close: props.on_close,
1300                on_minimize: props.on_minimize,
1301                on_maximize: props.on_maximize,
1302                show_controls: props.show_controls,
1303                show_address_bar: props.show_address_bar,
1304                read_only: props.read_only,
1305                variant: props.variant,
1306                size: props.size,
1307                custom_buttons: props.custom_buttons,
1308                class: props.frame_class,
1309                container_class: props.container_class,
1310                input_class: props.input_class,
1311                refresh_button_style: props.refresh_button_style,
1312                refresh_button_aria_label: props.refresh_button_aria_label,
1313                icon_button_style: props.icon_button_style,
1314                address_wrapper_base_style: props.address_wrapper_base_style,
1315                header_base_style: props.header_base_style,
1316                on_close_mouse_over: props.on_close_mouse_over,
1317                on_close_mouse_out: props.on_close_mouse_out,
1318                on_close_focus: props.on_close_focus,
1319                on_close_blur: props.on_close_blur,
1320                close_class: props.close_class,
1321                close_svg_class: props.close_svg_class,
1322                close_path_class: props.close_path_class,
1323                close_button_type: props.close_button_type,
1324                close_aria_label: props.close_aria_label,
1325                close_title: props.close_title,
1326                close_tabindex: props.close_tabindex,
1327                on_minimize_mouse_over: props.on_minimize_mouse_over,
1328                on_minimize_mouse_out: props.on_minimize_mouse_out,
1329                on_minimize_focus: props.on_minimize_focus,
1330                on_minimize_blur: props.on_minimize_blur,
1331                minimize_class: props.minimize_class,
1332                minimize_svg_class: props.minimize_svg_class,
1333                minimize_path_class: props.minimize_path_class,
1334                minimize_button_type: props.minimize_button_type,
1335                minimize_aria_label: props.minimize_aria_label,
1336                minimize_title: props.minimize_title,
1337                minimize_tabindex: props.minimize_tabindex,
1338                on_maximize_mouse_over: props.on_maximize_mouse_over,
1339                on_maximize_mouse_out: props.on_maximize_mouse_out,
1340                on_maximize_focus: props.on_maximize_focus,
1341                on_maximize_blur: props.on_maximize_blur,
1342                maximize_class: props.maximize_class,
1343                maximize_svg_class: props.maximize_svg_class,
1344                maximize_path_class: props.maximize_path_class,
1345                maximize_button_type: props.maximize_button_type,
1346                maximize_aria_label: props.maximize_aria_label,
1347                maximize_title: props.maximize_title,
1348                maximize_tabindex: props.maximize_tabindex,
1349                share_button_style: props.share_button_style,
1350                share_onclick: props.share_onclick,
1351                share_onmouseover: props.share_onmouseover,
1352                share_onmouseout: props.share_onmouseout,
1353                share_onfocus: props.share_onfocus,
1354                share_onblur: props.share_onblur,
1355                share_tabindex: props.share_tabindex,
1356                tabs_button_style: props.tabs_button_style,
1357                tabs_onclick: props.tabs_onclick,
1358                tabs_onmouseover: props.tabs_onmouseover,
1359                tabs_onmouseout: props.tabs_onmouseout,
1360                tabs_onfocus: props.tabs_onfocus,
1361                tabs_onblur: props.tabs_onblur,
1362                tabs_tabindex: props.tabs_tabindex,
1363                more_button_style: props.more_button_style,
1364                more_onclick: props.more_onclick,
1365                more_onmouseover: props.more_onmouseover,
1366                more_onmouseout: props.more_onmouseout,
1367                more_onfocus: props.more_onfocus,
1368                more_onblur: props.more_onblur,
1369                more_tabindex: props.more_tabindex,
1370            }
1371            BrowserContent {
1372                aria_describedby: props.aria_describedby,
1373                {props.children}
1374            }
1375        }
1376    }
1377}