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_attribute("style", "position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;").unwrap();
98 live_region.set_text_content(Some(&format!("Navigating to {}", value)));
99 document.body().unwrap().append_child(&live_region).unwrap();
100
101 let live_region_clone = live_region.clone();
102 Timeout::new(1000, move || {
103 let _ = document.body().unwrap().remove_child(&live_region_clone);
104 })
105 .forget();
106 }
107 })
108 };
109
110 let on_focus = {
111 let is_focused = is_focused.clone();
112 Callback::from(move |_| {
113 is_focused.set(true);
114 })
115 };
116
117 let on_blur = {
118 let is_focused = is_focused.clone();
119 Callback::from(move |_| {
120 is_focused.set(false);
121 })
122 };
123
124 html! {
125 <div class={format!("{} {}", props.container_class, props.class)} style={props.style}>
126 <label
127 for={props.input_id}
128 class="sr-only"
129 style="position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;"
130 >
131 { props.label }
132 </label>
133 <input
134 ref={input_ref.clone()}
135 id={props.input_id}
136 type="text"
137 value={(*input_value).clone()}
138 oninput={on_input_change}
139 onkeydown={on_key_down}
140 onfocus={on_focus}
141 onblur={on_blur}
142 placeholder={props.placeholder}
143 readonly={props.read_only}
144 class={props.input_class}
145 style={props.input_style}
146 aria-describedby={props.describedby}
147 autocomplete="url"
148 spellcheck={Some("false")}
149 />
150 <button
151 style={props.refresh_button_style}
152 aria-label={props.refresh_button_aria_label}
153 onclick={Callback::from(|_| {
154 let _ = web_sys::window().unwrap().location().reload();
155 })}
156 >
157 <svg
158 width="11"
159 height="13"
160 viewBox="0 0 11 13"
161 fill="none"
162 xmlns="http://www.w3.org/2000/svg"
163 >
164 <path
165 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"
166 stroke="#767676"
167 stroke-linecap="round"
168 stroke-linejoin="round"
169 />
170 </svg>
171 </button>
172 </div>
173 }
174}
175
176#[derive(Properties, PartialEq, Clone)]
177pub struct BrowserContentProps {
178 #[prop_or_default]
179 pub children: Children,
180 #[prop_or_default]
181 pub class: &'static str,
182 #[prop_or_default]
183 pub style: &'static str,
184 #[prop_or("Browser content area")]
185 pub aria_label: &'static str,
186 #[prop_or_default]
187 pub aria_describedby: &'static str,
188}
189
190#[function_component(BrowserContent)]
191pub fn browser_content(props: &BrowserContentProps) -> Html {
192 html! {
193 <main
194 class={props.class}
195 style={props.style}
196 role="main"
197 aria-label={props.aria_label}
198 aria-describedby={props.aria_describedby}
199 tabindex={Some("-1")}
200 >
201 { for props.children.iter() }
202 </main>
203 }
204}
205
206#[derive(Properties, PartialEq, Clone)]
207pub struct ControlButtonProps {
208 pub r#type: ButtonType,
209
210 #[prop_or_default]
211 pub on_click: Callback<()>,
212 #[prop_or_default]
213 pub on_mouse_over: Callback<()>,
214 #[prop_or_default]
215 pub on_mouse_out: Callback<()>,
216 #[prop_or_default]
217 pub on_focus: Callback<FocusEvent>,
218 #[prop_or_default]
219 pub on_blur: Callback<FocusEvent>,
220
221 #[prop_or(
222 "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;"
223 )]
224 pub style: &'static str,
225 #[prop_or_default]
226 pub class: &'static str,
227 #[prop_or_default]
228 pub svg_class: &'static str,
229 #[prop_or_default]
230 pub path_class: &'static str,
231
232 #[prop_or("button")]
233 pub button_type: &'static str,
234 #[prop_or_default]
235 pub aria_label: &'static str,
236 #[prop_or_default]
237 pub title: &'static str,
238 #[prop_or("0")]
239 pub tabindex: &'static str,
240}
241
242#[function_component(ControlButton)]
243pub fn control_button(props: &ControlButtonProps) -> Html {
244 let ControlButtonProps {
245 r#type,
246 on_click,
247 on_mouse_over,
248 on_mouse_out,
249 on_focus,
250 on_blur,
251 class,
252 style,
253 svg_class,
254 path_class,
255 button_type,
256 aria_label,
257 title,
258 tabindex,
259 } = props.clone();
260
261 let aria_label = if aria_label.is_empty() {
262 r#type.default_aria_label()
263 } else {
264 aria_label
265 };
266
267 let title = if title.is_empty() {
268 r#type.default_title()
269 } else {
270 title
271 };
272
273 let (fill, stroke) = match r#type {
274 ButtonType::Close => ("#FF5F57", "#E14640"),
275 ButtonType::Minimize => ("#FFBD2E", "#DFA123"),
276 ButtonType::Maximize => ("#28CA42", "#1DAD2C"),
277 };
278 let onclick = Callback::from(move |_| on_click.emit(()));
279 let onmouseover = Callback::from(move |_| on_mouse_over.emit(()));
280 let onmouseout = Callback::from(move |_| on_mouse_out.emit(()));
281
282 html! {
283 <button
284 type={button_type}
285 class={class}
286 style={style}
287 onclick={onclick}
288 onmouseover={onmouseover}
289 onmouseout={onmouseout}
290 onfocus={on_focus}
291 onblur={on_blur}
292 aria-label={aria_label}
293 title={title}
294 tabindex={tabindex}
295 >
296 <svg
297 class={svg_class}
298 width="12"
299 height="12"
300 viewBox="0 0 12 12"
301 fill="none"
302 xmlns="http://www.w3.org/2000/svg"
303 >
304 <path
305 class={path_class}
306 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"
307 fill={fill}
308 stroke={stroke}
309 />
310 </svg>
311 </button>
312 }
313}
314
315#[derive(Properties, PartialEq, Clone)]
316pub struct BrowserControlsProps {
317 #[prop_or_default]
318 pub show_controls: bool,
319 #[prop_or_default]
320 pub class: &'static str,
321 #[prop_or("display: flex; align-items: center; background: none; padding-left: 10px;")]
322 pub style: &'static str,
323
324 #[prop_or_default]
325 pub on_close: Callback<()>,
326 #[prop_or_default]
327 pub on_close_mouse_over: Callback<()>,
328 #[prop_or_default]
329 pub on_close_mouse_out: Callback<()>,
330 #[prop_or_default]
331 pub on_close_focus: Callback<FocusEvent>,
332 #[prop_or_default]
333 pub on_close_blur: Callback<FocusEvent>,
334 #[prop_or_default]
335 pub close_class: &'static str,
336 #[prop_or_default]
337 pub close_svg_class: &'static str,
338 #[prop_or_default]
339 pub close_path_class: &'static str,
340 #[prop_or("button")]
341 pub close_button_type: &'static str,
342 #[prop_or_default]
343 pub close_aria_label: &'static str,
344 #[prop_or_default]
345 pub close_title: &'static str,
346 #[prop_or("0")]
347 pub close_tabindex: &'static str,
348
349 #[prop_or_default]
350 pub on_minimize: Callback<()>,
351 #[prop_or_default]
352 pub on_minimize_mouse_over: Callback<()>,
353 #[prop_or_default]
354 pub on_minimize_mouse_out: Callback<()>,
355 #[prop_or_default]
356 pub on_minimize_focus: Callback<FocusEvent>,
357 #[prop_or_default]
358 pub on_minimize_blur: Callback<FocusEvent>,
359 #[prop_or_default]
360 pub minimize_class: &'static str,
361 #[prop_or_default]
362 pub minimize_svg_class: &'static str,
363 #[prop_or_default]
364 pub minimize_path_class: &'static str,
365 #[prop_or("button")]
366 pub minimize_button_type: &'static str,
367 #[prop_or_default]
368 pub minimize_aria_label: &'static str,
369 #[prop_or_default]
370 pub minimize_title: &'static str,
371 #[prop_or("0")]
372 pub minimize_tabindex: &'static str,
373
374 #[prop_or_default]
375 pub on_maximize: Callback<()>,
376 #[prop_or_default]
377 pub on_maximize_mouse_over: Callback<()>,
378 #[prop_or_default]
379 pub on_maximize_mouse_out: Callback<()>,
380 #[prop_or_default]
381 pub on_maximize_focus: Callback<FocusEvent>,
382 #[prop_or_default]
383 pub on_maximize_blur: Callback<FocusEvent>,
384 #[prop_or_default]
385 pub maximize_class: &'static str,
386 #[prop_or_default]
387 pub maximize_svg_class: &'static str,
388 #[prop_or_default]
389 pub maximize_path_class: &'static str,
390 #[prop_or("button")]
391 pub maximize_button_type: &'static str,
392 #[prop_or_default]
393 pub maximize_aria_label: &'static str,
394 #[prop_or_default]
395 pub maximize_title: &'static str,
396 #[prop_or("0")]
397 pub maximize_tabindex: &'static str,
398}
399
400#[function_component(BrowserControls)]
401pub fn browser_controls(props: &BrowserControlsProps) -> Html {
402 if !props.show_controls {
403 return html! {};
404 }
405
406 html! {
407 <nav
408 class={props.class}
409 style={props.style}
410 role="toolbar"
411 aria-label="Browser window controls"
412 >
413 <ControlButton
414 r#type={ButtonType::Close}
415 on_click={props.on_close.clone()}
416 on_mouse_over={props.on_close_mouse_over.clone()}
417 on_mouse_out={props.on_close_mouse_out.clone()}
418 on_focus={props.on_close_focus.clone()}
419 on_blur={props.on_close_blur.clone()}
420 class={props.close_class}
421 svg_class={props.close_svg_class}
422 path_class={props.close_path_class}
423 button_type={props.close_button_type}
424 aria_label={props.close_aria_label}
425 title={props.close_title}
426 tabindex={props.close_tabindex}
427 />
428 <ControlButton
429 r#type={ButtonType::Minimize}
430 on_click={props.on_minimize.clone()}
431 on_mouse_over={props.on_minimize_mouse_over.clone()}
432 on_mouse_out={props.on_minimize_mouse_out.clone()}
433 on_focus={props.on_minimize_focus.clone()}
434 on_blur={props.on_minimize_blur.clone()}
435 class={props.minimize_class}
436 svg_class={props.minimize_svg_class}
437 path_class={props.minimize_path_class}
438 button_type={props.minimize_button_type}
439 aria_label={props.minimize_aria_label}
440 title={props.minimize_title}
441 tabindex={props.minimize_tabindex}
442 />
443 <ControlButton
444 r#type={ButtonType::Maximize}
445 on_click={props.on_maximize.clone()}
446 on_mouse_over={props.on_maximize_mouse_over.clone()}
447 on_mouse_out={props.on_maximize_mouse_out.clone()}
448 on_focus={props.on_maximize_focus.clone()}
449 on_blur={props.on_maximize_blur.clone()}
450 class={props.maximize_class}
451 svg_class={props.maximize_svg_class}
452 path_class={props.maximize_path_class}
453 button_type={props.maximize_button_type}
454 aria_label={props.maximize_aria_label}
455 title={props.maximize_title}
456 tabindex={props.maximize_tabindex}
457 />
458 </nav>
459 }
460}
461
462#[derive(Properties, PartialEq, Clone)]
463pub struct BrowserHeaderProps {
464 #[prop_or_default]
465 pub url: String,
466 #[prop_or_default]
467 pub placeholder: &'static str,
468 #[prop_or_default]
469 pub on_url_change: Option<Callback<InputEvent>>,
470 #[prop_or(true)]
471 pub show_controls: bool,
472 #[prop_or(true)]
473 pub show_address_bar: bool,
474 #[prop_or(false)]
475 pub read_only: bool,
476 #[prop_or_default]
477 pub variant: Variant,
478 #[prop_or_default]
479 pub size: Size,
480 #[prop_or_default]
481 pub custom_buttons: Vec<Html>,
482 #[prop_or_default]
483 pub class: &'static str,
484
485 #[prop_or_default]
486 pub container_class: &'static str,
487 #[prop_or("text-black dark:text-white")]
488 pub input_class: &'static str,
489 #[prop_or_default]
490 pub refresh_button_style: &'static str,
491 #[prop_or("Refresh")]
492 pub refresh_button_aria_label: &'static str,
493
494 #[prop_or(
495 "padding: 4px; cursor: pointer; background: none; border: none; box-shadow: none; outline: none;"
496 )]
497 pub icon_button_style: &'static str,
498
499 #[prop_or("flex: 1; display: flex; justify-content: center; padding-right: 8px;")]
500 pub address_wrapper_base_style: &'static str,
501
502 #[prop_or("display: flex; align-items: center; position: relative;")]
503 pub header_base_style: &'static str,
504
505 #[prop_or_default]
506 pub on_close: Callback<()>,
507 #[prop_or_default]
508 pub on_close_mouse_over: Callback<()>,
509 #[prop_or_default]
510 pub on_close_mouse_out: Callback<()>,
511 #[prop_or_default]
512 pub on_close_focus: Callback<FocusEvent>,
513 #[prop_or_default]
514 pub on_close_blur: Callback<FocusEvent>,
515 #[prop_or_default]
516 pub close_class: &'static str,
517 #[prop_or_default]
518 pub close_svg_class: &'static str,
519 #[prop_or_default]
520 pub close_path_class: &'static str,
521 #[prop_or("button")]
522 pub close_button_type: &'static str,
523 #[prop_or_default]
524 pub close_aria_label: &'static str,
525 #[prop_or_default]
526 pub close_title: &'static str,
527 #[prop_or("0")]
528 pub close_tabindex: &'static str,
529
530 #[prop_or_default]
531 pub on_minimize: Callback<()>,
532 #[prop_or_default]
533 pub on_minimize_mouse_over: Callback<()>,
534 #[prop_or_default]
535 pub on_minimize_mouse_out: Callback<()>,
536 #[prop_or_default]
537 pub on_minimize_focus: Callback<FocusEvent>,
538 #[prop_or_default]
539 pub on_minimize_blur: Callback<FocusEvent>,
540 #[prop_or_default]
541 pub minimize_class: &'static str,
542 #[prop_or_default]
543 pub minimize_svg_class: &'static str,
544 #[prop_or_default]
545 pub minimize_path_class: &'static str,
546 #[prop_or("button")]
547 pub minimize_button_type: &'static str,
548 #[prop_or_default]
549 pub minimize_aria_label: &'static str,
550 #[prop_or_default]
551 pub minimize_title: &'static str,
552 #[prop_or("0")]
553 pub minimize_tabindex: &'static str,
554
555 #[prop_or_default]
556 pub on_maximize: Callback<()>,
557 #[prop_or_default]
558 pub on_maximize_mouse_over: Callback<()>,
559 #[prop_or_default]
560 pub on_maximize_mouse_out: Callback<()>,
561 #[prop_or_default]
562 pub on_maximize_focus: Callback<FocusEvent>,
563 #[prop_or_default]
564 pub on_maximize_blur: Callback<FocusEvent>,
565 #[prop_or_default]
566 pub maximize_class: &'static str,
567 #[prop_or_default]
568 pub maximize_svg_class: &'static str,
569 #[prop_or_default]
570 pub maximize_path_class: &'static str,
571 #[prop_or("button")]
572 pub maximize_button_type: &'static str,
573 #[prop_or_default]
574 pub maximize_aria_label: &'static str,
575 #[prop_or_default]
576 pub maximize_title: &'static str,
577 #[prop_or("0")]
578 pub maximize_tabindex: &'static str,
579
580 #[prop_or_default]
581 pub share_button_style: &'static str,
582 #[prop_or_default]
583 pub share_onclick: Callback<()>,
584 #[prop_or_default]
585 pub share_onmouseover: Callback<()>,
586 #[prop_or_default]
587 pub share_onmouseout: Callback<()>,
588 #[prop_or_default]
589 pub share_onfocus: Callback<FocusEvent>,
590 #[prop_or_default]
591 pub share_onblur: Callback<FocusEvent>,
592 #[prop_or_default]
593 pub share_tabindex: &'static str,
594
595 #[prop_or_default]
596 pub tabs_button_style: &'static str,
597 #[prop_or_default]
598 pub tabs_onclick: Callback<()>,
599 #[prop_or_default]
600 pub tabs_onmouseover: Callback<()>,
601 #[prop_or_default]
602 pub tabs_onmouseout: Callback<()>,
603 #[prop_or_default]
604 pub tabs_onfocus: Callback<FocusEvent>,
605 #[prop_or_default]
606 pub tabs_onblur: Callback<FocusEvent>,
607 #[prop_or_default]
608 pub tabs_tabindex: &'static str,
609
610 #[prop_or_default]
611 pub more_button_style: &'static str,
612 #[prop_or_default]
613 pub more_onclick: Callback<()>,
614 #[prop_or_default]
615 pub more_onmouseover: Callback<()>,
616 #[prop_or_default]
617 pub more_onmouseout: Callback<()>,
618 #[prop_or_default]
619 pub more_onfocus: Callback<FocusEvent>,
620 #[prop_or_default]
621 pub more_onblur: Callback<FocusEvent>,
622 #[prop_or_default]
623 pub more_tabindex: &'static str,
624}
625
626#[function_component(BrowserHeader)]
627pub fn browser_header(props: &BrowserHeaderProps) -> Html {
628 let is_ios = props.variant == Variant::Ios;
629 let is_tabs = props.variant == Variant::Tabs;
630
631 let base_style = {
632 let padding = match props.size {
633 Size::Small => "4px 6px",
634 Size::Large => "10px 16px",
635 _ => "6px 12px",
636 };
637 let height = match (props.variant.clone(), props.size.clone()) {
638 (Variant::Tabs, _) => "40px",
639 (Variant::Ios, _) => "56px",
640 (_, Size::Large) => "60px",
641 (_, Size::Small) => "38px",
642 _ => "48px",
643 };
644 let border_radius = if is_tabs {
645 "6px"
646 } else if props.variant == Variant::Default {
647 "8px 8px 0 0"
648 } else {
649 "0"
650 };
651 let border = if is_tabs { "1px solid #d1d5db" } else { "none" };
652 let box_shadow = if props.variant == Variant::Default {
653 "0 2px 6px rgba(0,0,0,0.1)"
654 } else {
655 "none"
656 };
657
658 format!(
659 "{} justify-content: {}; padding: {}; height: {}; border-radius: {}; border: {}; box-shadow: {};",
660 props.header_base_style,
661 if is_ios {
662 "space-between"
663 } else {
664 "flex-start"
665 },
666 padding,
667 height,
668 border_radius,
669 border,
670 box_shadow
671 )
672 };
673
674 let address_wrapper_style = format!(
675 "{} padding-left: {};",
676 props.address_wrapper_base_style,
677 if props.show_controls { "8px" } else { "0" }
678 );
679 let share_onclick = props.share_onclick.clone();
680 let share_onmouseover = props.share_onmouseover.clone();
681 let share_onmouseout = props.share_onmouseout.clone();
682
683 let tabs_onclick = props.tabs_onclick.clone();
684 let tabs_onmouseover = props.tabs_onmouseover.clone();
685 let tabs_onmouseout = props.tabs_onmouseout.clone();
686
687 let more_onclick = props.more_onclick.clone();
688 let more_onmouseover = props.more_onmouseover.clone();
689 let more_onmouseout = props.more_onmouseout.clone();
690
691 let share_onclick = Callback::from(move |_| share_onclick.emit(()));
692 let share_onmouseover = Callback::from(move |_| share_onmouseover.emit(()));
693 let share_onmouseout = Callback::from(move |_| share_onmouseout.emit(()));
694
695 let tabs_onclick = Callback::from(move |_| tabs_onclick.emit(()));
696 let tabs_onmouseover = Callback::from(move |_| tabs_onmouseover.emit(()));
697 let tabs_onmouseout = Callback::from(move |_| tabs_onmouseout.emit(()));
698
699 let more_onclick = Callback::from(move |_| more_onclick.emit(()));
700 let more_onmouseover = Callback::from(move |_| more_onmouseover.emit(()));
701 let more_onmouseout = Callback::from(move |_| more_onmouseout.emit(()));
702
703 html! {
704 <header style={base_style} class={props.class} aria-label="Browser window header">
705 <div style="display: flex; align-items: center; gap: 6px;">
706 if props.show_controls {
707 <BrowserControls
708 on_close={props.on_close.clone()}
709 on_minimize={props.on_minimize.clone()}
710 on_maximize={props.on_maximize.clone()}
711 show_controls={props.show_controls}
712 on_close={props.on_close.clone()}
713 on_close_mouse_over={props.on_close_mouse_over.clone()}
714 on_close_mouse_out={props.on_close_mouse_out.clone()}
715 on_close_focus={props.on_close_focus.clone()}
716 on_close_blur={props.on_close_blur.clone()}
717 close_class={props.close_class}
718 close_svg_class={props.close_svg_class}
719 close_path_class={props.close_path_class}
720 close_button_type={props.close_button_type}
721 close_aria_label={props.close_aria_label}
722 close_title={props.close_title}
723 close_tabindex={props.close_tabindex}
724 on_minimize={props.on_minimize.clone()}
725 on_minimize_mouse_over={props.on_minimize_mouse_over.clone()}
726 on_minimize_mouse_out={props.on_minimize_mouse_out.clone()}
727 on_minimize_focus={props.on_minimize_focus.clone()}
728 on_minimize_blur={props.on_minimize_blur.clone()}
729 minimize_class={props.minimize_class}
730 minimize_svg_class={props.minimize_svg_class}
731 minimize_path_class={props.minimize_path_class}
732 minimize_button_type={props.minimize_button_type}
733 minimize_aria_label={props.minimize_aria_label}
734 minimize_title={props.minimize_title}
735 minimize_tabindex={props.minimize_tabindex}
736 on_maximize={props.on_maximize.clone()}
737 on_maximize_mouse_over={props.on_maximize_mouse_over.clone()}
738 on_maximize_mouse_out={props.on_maximize_mouse_out.clone()}
739 on_maximize_focus={props.on_maximize_focus.clone()}
740 on_maximize_blur={props.on_maximize_blur.clone()}
741 maximize_class={props.maximize_class}
742 maximize_svg_class={props.maximize_svg_class}
743 maximize_path_class={props.maximize_path_class}
744 maximize_button_type={props.maximize_button_type}
745 maximize_aria_label={props.maximize_aria_label}
746 maximize_title={props.maximize_title}
747 maximize_tabindex={props.maximize_tabindex}
748 />
749 }
750 if props.show_controls {
751 if !is_ios {
752 <button style={props.icon_button_style} aria-label="Sidebar">
753 <svg
754 width="20"
755 height="15"
756 viewBox="0 0 20 15"
757 fill="none"
758 xmlns="http://www.w3.org/2000/svg"
759 >
760 <path
761 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"
762 fill="#767676"
763 />
764 </svg>
765 </button>
766 <button style={props.icon_button_style} aria-label="Back">
767 <svg
768 width="9"
769 height="16"
770 viewBox="0 0 9 16"
771 fill="none"
772 xmlns="http://www.w3.org/2000/svg"
773 >
774 <path
775 d="M7.5 1.5L1 8L7.5 14.5"
776 stroke="#737373"
777 stroke-width="1.5"
778 stroke-linecap="round"
779 stroke-linejoin="round"
780 />
781 </svg>
782 </button>
783 <button style={props.icon_button_style} aria-label="Forward">
784 <svg
785 width="9"
786 height="16"
787 viewBox="0 0 9 16"
788 fill="none"
789 xmlns="http://www.w3.org/2000/svg"
790 >
791 <path
792 d="M1 14.5L7.5 8L1 1.5"
793 stroke="#BFBFBF"
794 stroke-width="1.5"
795 stroke-linecap="round"
796 stroke-linejoin="round"
797 />
798 </svg>
799 </button>
800 }
801 }
802 </div>
803 if props.show_address_bar {
804 <div style={address_wrapper_style}>
805 <AddressBar
806 url={props.url.clone()}
807 placeholder={props.placeholder}
808 on_url_change={props.on_url_change.clone().unwrap_or_default()}
809 read_only={props.read_only}
810 input_class={props.input_class}
811 container_class={props.container_class}
812 refresh_button_style={props.refresh_button_style}
813 refresh_button_aria_label={props.refresh_button_aria_label}
814 />
815 </div>
816 }
817 <div style="display: flex; align-items: center; gap: 6px; margin-left: auto;">
818 if props.show_controls {
819 { for props.custom_buttons.iter().cloned() }
820 <button
821 style={props.icon_button_style}
822 onclick={share_onclick.clone()}
823 onmouseover={share_onmouseover.clone()}
824 onmouseout={share_onmouseout.clone()}
825 onfocus={props.share_onfocus.clone()}
826 onblur={props.share_onblur.clone()}
827 aria-label="Share"
828 title="Share"
829 tabindex={props.share_tabindex}
830 >
831 <svg
832 width="15"
833 height="19"
834 viewBox="0 0 15 19"
835 fill="none"
836 xmlns="http://www.w3.org/2000/svg"
837 >
838 <path
839 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"
840 fill="#767676"
841 />
842 </svg>
843 </button>
844 <button
845 style={props.icon_button_style}
846 onclick={tabs_onclick.clone()}
847 onmouseover={tabs_onmouseover.clone()}
848 onmouseout={tabs_onmouseout.clone()}
849 onfocus={props.tabs_onfocus.clone()}
850 onblur={props.tabs_onblur.clone()}
851 aria-label="Tabs"
852 title="Tabs"
853 tabindex={props.tabs_tabindex}
854 >
855 <svg
856 width="15"
857 height="15"
858 viewBox="0 0 15 15"
859 fill="none"
860 xmlns="http://www.w3.org/2000/svg"
861 >
862 <path
863 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"
864 fill="#767676"
865 />
866 </svg>
867 </button>
868 <button
869 style={props.icon_button_style}
870 onclick={more_onclick.clone()}
871 onmouseover={more_onmouseover.clone()}
872 onmouseout={more_onmouseout.clone()}
873 onfocus={props.more_onfocus.clone()}
874 onblur={props.more_onblur.clone()}
875 aria-label="More options"
876 title="More options"
877 tabindex={props.more_tabindex}
878 >
879 <svg
880 width="18"
881 height="19"
882 viewBox="0 0 18 19"
883 fill="none"
884 xmlns="http://www.w3.org/2000/svg"
885 >
886 <path
887 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"
888 fill="#767676"
889 />
890 </svg>
891 </button>
892 }
893 </div>
894 </header>
895 }
896}
897
898#[derive(Clone, PartialEq)]
899pub struct KeyboardNavigationOptions {
900 pub on_escape: Option<Callback<()>>,
901 pub on_enter: Option<Callback<()>>,
902 pub trap_focus: bool,
903}
904
905#[hook]
906pub fn use_keyboard(options: KeyboardNavigationOptions) -> NodeRef {
907 let container_ref = use_node_ref();
908
909 {
910 let options = options.clone();
911 let container_ref = container_ref.clone();
912
913 use_effect(move || {
914 let closure = Closure::<dyn Fn(KeyboardEvent)>::wrap(Box::new(
915 move |event: KeyboardEvent| {
916 let key = event.key();
917 let target = event.target();
918
919 match key.as_str() {
920 "Escape" => {
921 if let Some(callback) = &options.on_escape {
922 event.prevent_default();
923 callback.emit(());
924 }
925 }
926 "Enter" => {
927 if let Some(callback) = &options.on_enter {
928 if let Some(target_elem) =
929 target.and_then(|t| t.dyn_into::<Element>().ok())
930 {
931 if Some(target_elem) == container_ref.cast::<Element>() {
932 event.prevent_default();
933 callback.emit(());
934 }
935 }
936 }
937 }
938 "Tab" if options.trap_focus => {
939 if let Some(container) = container_ref.cast::<Element>() {
940 let selector = "button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])";
941 let focusables = container.query_selector_all(selector).unwrap();
942
943 let length = focusables.length();
944 if length == 0 {
945 return;
946 }
947
948 let first = focusables
949 .item(0)
950 .and_then(|e| e.dyn_into::<web_sys::HtmlElement>().ok());
951 let last = focusables
952 .item(length - 1)
953 .and_then(|e| e.dyn_into::<web_sys::HtmlElement>().ok());
954
955 let document = web_sys::window().unwrap().document().unwrap();
956 let active = document.active_element();
957
958 if event.shift_key() {
959 if active == first.as_ref().map(|e| e.clone().into()) {
960 event.prevent_default();
961 if let Some(elem) = last {
962 elem.focus().ok();
963 }
964 }
965 } else if active == last.as_ref().map(|e| e.clone().into()) {
966 event.prevent_default();
967 if let Some(elem) = first {
968 elem.focus().ok();
969 }
970 }
971 }
972 }
973 _ => {}
974 }
975 },
976 )
977 as Box<dyn Fn(KeyboardEvent)>);
978
979 web_sys::window()
980 .unwrap()
981 .add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref())
982 .unwrap();
983
984 move || {
985 web_sys::window()
986 .unwrap()
987 .remove_event_listener_with_callback(
988 "keydown",
989 closure.as_ref().unchecked_ref(),
990 )
991 .unwrap();
992 drop(closure);
993 }
994 });
995 }
996
997 container_ref
998}
999
1000#[derive(Properties, PartialEq, Clone)]
1005pub struct BrowserFrameProps {
1006 #[prop_or_default]
1008 pub children: Children,
1009
1010 #[prop_or_default]
1012 pub url: String,
1013
1014 #[prop_or_default]
1016 pub placeholder: &'static str,
1017
1018 #[prop_or_default]
1020 pub on_url_change: Option<Callback<InputEvent>>,
1021
1022 #[prop_or_default]
1024 pub on_close: Callback<()>,
1025
1026 #[prop_or_default]
1028 pub on_minimize: Callback<()>,
1029
1030 #[prop_or_default]
1032 pub on_maximize: Callback<()>,
1033
1034 #[prop_or(true)]
1038 pub show_controls: bool,
1039
1040 #[prop_or(true)]
1044 pub show_address_bar: bool,
1045
1046 #[prop_or(false)]
1050 pub read_only: bool,
1051
1052 #[prop_or_default]
1054 pub size: Size,
1055
1056 #[prop_or_default]
1058 pub variant: Variant,
1059
1060 #[prop_or_default]
1062 pub custom_buttons: Vec<Html>,
1063
1064 #[prop_or(
1068 "rounded-lg border shadow-lg overflow-hidden bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700"
1069 )]
1070 pub class: &'static str,
1071
1072 #[prop_or_default]
1074 pub frame_class: &'static str,
1075
1076 #[prop_or_default]
1078 pub style: &'static str,
1079
1080 #[prop_or_default]
1082 pub id: &'static str,
1083
1084 #[prop_or("Browser window")]
1088 pub aria_label: &'static str,
1089
1090 #[prop_or_default]
1092 pub aria_describedby: &'static str,
1093
1094 #[prop_or_default]
1096 pub container_class: &'static str,
1097
1098 #[prop_or("text-black dark:text-white")]
1100 pub input_class: &'static str,
1101
1102 #[prop_or(
1106 "position: absolute; top: 50%; right: 8px; transform: translateY(-50%); padding: 4px; background: none; border: none; box-shadow: none; outline: none; cursor: pointer;"
1107 )]
1108 pub refresh_button_style: &'static str,
1109
1110 #[prop_or("Refresh")]
1114 pub refresh_button_aria_label: &'static str,
1115
1116 #[prop_or(
1120 "padding: 4px; cursor: pointer; background: none; border: none; box-shadow: none; outline: none;"
1121 )]
1122 pub icon_button_style: &'static str,
1123
1124 #[prop_or("flex: 1; display: flex; justify-content: center; padding-right: 8px;")]
1128 pub address_wrapper_base_style: &'static str,
1129
1130 #[prop_or("display: flex; align-items: center; position: relative;")]
1134 pub header_base_style: &'static str,
1135
1136 #[prop_or_default]
1138 pub on_close_mouse_over: Callback<()>,
1139 #[prop_or_default]
1140 pub on_close_mouse_out: Callback<()>,
1141 #[prop_or_default]
1142 pub on_close_focus: Callback<FocusEvent>,
1143 #[prop_or_default]
1144 pub on_close_blur: Callback<FocusEvent>,
1145 #[prop_or_default]
1146 pub close_class: &'static str,
1147 #[prop_or_default]
1148 pub close_svg_class: &'static str,
1149 #[prop_or_default]
1150 pub close_path_class: &'static str,
1151 #[prop_or("button")]
1152 pub close_button_type: &'static str,
1153 #[prop_or_default]
1154 pub close_aria_label: &'static str,
1155 #[prop_or_default]
1156 pub close_title: &'static str,
1157 #[prop_or("0")]
1158 pub close_tabindex: &'static str,
1159
1160 #[prop_or_default]
1162 pub on_minimize_mouse_over: Callback<()>,
1163 #[prop_or_default]
1164 pub on_minimize_mouse_out: Callback<()>,
1165 #[prop_or_default]
1166 pub on_minimize_focus: Callback<FocusEvent>,
1167 #[prop_or_default]
1168 pub on_minimize_blur: Callback<FocusEvent>,
1169 #[prop_or_default]
1170 pub minimize_class: &'static str,
1171 #[prop_or_default]
1172 pub minimize_svg_class: &'static str,
1173 #[prop_or_default]
1174 pub minimize_path_class: &'static str,
1175 #[prop_or("button")]
1176 pub minimize_button_type: &'static str,
1177 #[prop_or_default]
1178 pub minimize_aria_label: &'static str,
1179 #[prop_or_default]
1180 pub minimize_title: &'static str,
1181 #[prop_or("0")]
1182 pub minimize_tabindex: &'static str,
1183
1184 #[prop_or_default]
1186 pub on_maximize_mouse_over: Callback<()>,
1187 #[prop_or_default]
1188 pub on_maximize_mouse_out: Callback<()>,
1189 #[prop_or_default]
1190 pub on_maximize_focus: Callback<FocusEvent>,
1191 #[prop_or_default]
1192 pub on_maximize_blur: Callback<FocusEvent>,
1193 #[prop_or_default]
1194 pub maximize_class: &'static str,
1195 #[prop_or_default]
1196 pub maximize_svg_class: &'static str,
1197 #[prop_or_default]
1198 pub maximize_path_class: &'static str,
1199 #[prop_or("button")]
1200 pub maximize_button_type: &'static str,
1201 #[prop_or_default]
1202 pub maximize_aria_label: &'static str,
1203 #[prop_or_default]
1204 pub maximize_title: &'static str,
1205 #[prop_or("0")]
1206 pub maximize_tabindex: &'static str,
1207
1208 #[prop_or_default]
1210 pub share_button_style: &'static str,
1211 #[prop_or_default]
1212 pub share_onclick: Callback<()>,
1213 #[prop_or_default]
1214 pub share_onmouseover: Callback<()>,
1215 #[prop_or_default]
1216 pub share_onmouseout: Callback<()>,
1217 #[prop_or_default]
1218 pub share_onfocus: Callback<FocusEvent>,
1219 #[prop_or_default]
1220 pub share_onblur: Callback<FocusEvent>,
1221 #[prop_or_default]
1222 pub share_tabindex: &'static str,
1223
1224 #[prop_or_default]
1226 pub tabs_button_style: &'static str,
1227 #[prop_or_default]
1228 pub tabs_onclick: Callback<()>,
1229 #[prop_or_default]
1230 pub tabs_onmouseover: Callback<()>,
1231 #[prop_or_default]
1232 pub tabs_onmouseout: Callback<()>,
1233 #[prop_or_default]
1234 pub tabs_onfocus: Callback<FocusEvent>,
1235 #[prop_or_default]
1236 pub tabs_onblur: Callback<FocusEvent>,
1237 #[prop_or_default]
1238 pub tabs_tabindex: &'static str,
1239
1240 #[prop_or_default]
1242 pub more_button_style: &'static str,
1243 #[prop_or_default]
1244 pub more_onclick: Callback<()>,
1245 #[prop_or_default]
1246 pub more_onmouseover: Callback<()>,
1247 #[prop_or_default]
1248 pub more_onmouseout: Callback<()>,
1249 #[prop_or_default]
1250 pub more_onfocus: Callback<FocusEvent>,
1251 #[prop_or_default]
1252 pub more_onblur: Callback<FocusEvent>,
1253 #[prop_or_default]
1254 pub more_tabindex: &'static str,
1255}
1256#[function_component(BrowserFrame)]
1345pub fn browser_frame(props: &BrowserFrameProps) -> Html {
1346 let on_close = props.on_close.clone();
1347 let container_ref = use_keyboard(KeyboardNavigationOptions {
1348 on_escape: Some(Callback::from(move |_| on_close.emit(()))),
1349 on_enter: None,
1350 trap_focus: false,
1351 });
1352
1353 let size_style = props.size.to_style();
1354 let combined_style = format!("{} {}", size_style, props.style);
1355
1356 html! {
1357 <article
1358 ref={container_ref}
1359 id={props.id}
1360 class={props.class}
1361 style={combined_style}
1362 role="application"
1363 aria-label={props.aria_label}
1364 aria-describedby={props.aria_describedby}
1365 tabindex={Some("-1")}
1366 >
1367 <BrowserHeader
1368 url={props.url.clone()}
1369 placeholder={props.placeholder}
1370 on_url_change={props.on_url_change.clone()}
1371 on_close={props.on_close.clone()}
1372 on_minimize={props.on_minimize.clone()}
1373 on_maximize={props.on_maximize.clone()}
1374 show_controls={props.show_controls}
1375 show_address_bar={props.show_address_bar}
1376 read_only={props.read_only}
1377 variant={props.variant.clone()}
1378 size={props.size.clone()}
1379 custom_buttons={props.custom_buttons.clone()}
1380 class={props.frame_class}
1381 container_class={props.container_class}
1382 input_class={props.input_class}
1383 refresh_button_style={props.refresh_button_style}
1384 refresh_button_aria_label={props.refresh_button_aria_label}
1385 icon_button_style={props.icon_button_style}
1386 address_wrapper_base_style={props.address_wrapper_base_style}
1387 header_base_style={props.header_base_style}
1388 on_close_mouse_over={props.on_close_mouse_over.clone()}
1389 on_close_mouse_out={props.on_close_mouse_out.clone()}
1390 on_close_focus={props.on_close_focus.clone()}
1391 on_close_blur={props.on_close_blur.clone()}
1392 close_class={props.close_class}
1393 close_svg_class={props.close_svg_class}
1394 close_path_class={props.close_path_class}
1395 close_button_type={props.close_button_type}
1396 close_aria_label={props.close_aria_label}
1397 close_title={props.close_title}
1398 close_tabindex={props.close_tabindex}
1399 on_minimize_mouse_over={props.on_minimize_mouse_over.clone()}
1400 on_minimize_mouse_out={props.on_minimize_mouse_out.clone()}
1401 on_minimize_focus={props.on_minimize_focus.clone()}
1402 on_minimize_blur={props.on_minimize_blur.clone()}
1403 minimize_class={props.minimize_class}
1404 minimize_svg_class={props.minimize_svg_class}
1405 minimize_path_class={props.minimize_path_class}
1406 minimize_button_type={props.minimize_button_type}
1407 minimize_aria_label={props.minimize_aria_label}
1408 minimize_title={props.minimize_title}
1409 minimize_tabindex={props.minimize_tabindex}
1410 on_maximize_mouse_over={props.on_maximize_mouse_over.clone()}
1411 on_maximize_mouse_out={props.on_maximize_mouse_out.clone()}
1412 on_maximize_focus={props.on_maximize_focus.clone()}
1413 on_maximize_blur={props.on_maximize_blur.clone()}
1414 maximize_class={props.maximize_class}
1415 maximize_svg_class={props.maximize_svg_class}
1416 maximize_path_class={props.maximize_path_class}
1417 maximize_button_type={props.maximize_button_type}
1418 maximize_aria_label={props.maximize_aria_label}
1419 maximize_title={props.maximize_title}
1420 maximize_tabindex={props.maximize_tabindex}
1421 share_button_style={props.share_button_style}
1422 share_onclick={props.share_onclick.clone()}
1423 share_onmouseover={props.share_onmouseover.clone()}
1424 share_onmouseout={props.share_onmouseout.clone()}
1425 share_onfocus={props.share_onfocus.clone()}
1426 share_onblur={props.share_onblur.clone()}
1427 share_tabindex={props.share_tabindex}
1428 tabs_button_style={props.tabs_button_style}
1429 tabs_onclick={props.tabs_onclick.clone()}
1430 tabs_onmouseover={props.tabs_onmouseover.clone()}
1431 tabs_onmouseout={props.tabs_onmouseout.clone()}
1432 tabs_onfocus={props.tabs_onfocus.clone()}
1433 tabs_onblur={props.tabs_onblur.clone()}
1434 tabs_tabindex={props.tabs_tabindex}
1435 more_button_style={props.more_button_style}
1436 more_onclick={props.more_onclick.clone()}
1437 more_onmouseover={props.more_onmouseover.clone()}
1438 more_onmouseout={props.more_onmouseout.clone()}
1439 more_onfocus={props.more_onfocus.clone()}
1440 more_onblur={props.more_onblur.clone()}
1441 more_tabindex={props.more_tabindex}
1442 />
1443 <BrowserContent aria_describedby={props.aria_describedby}>
1444 { for props.children.iter() }
1445 </BrowserContent>
1446 </article>
1447 }
1448}