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