browser_rs/
yew.rs

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