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#[derive(Properties, PartialEq, Clone)]
993pub struct BrowserFrameProps {
994 #[prop_or_default]
996 pub children: Children,
997
998 #[prop_or_default]
1000 pub url: String,
1001
1002 #[prop_or_default]
1004 pub placeholder: &'static str,
1005
1006 #[prop_or_default]
1008 pub on_url_change: Option<Callback<InputEvent>>,
1009
1010 #[prop_or_default]
1012 pub on_close: Callback<()>,
1013
1014 #[prop_or_default]
1016 pub on_minimize: Callback<()>,
1017
1018 #[prop_or_default]
1020 pub on_maximize: Callback<()>,
1021
1022 #[prop_or(true)]
1026 pub show_controls: bool,
1027
1028 #[prop_or(true)]
1032 pub show_address_bar: bool,
1033
1034 #[prop_or(false)]
1038 pub read_only: bool,
1039
1040 #[prop_or_default]
1042 pub size: Size,
1043
1044 #[prop_or_default]
1046 pub variant: Variant,
1047
1048 #[prop_or_default]
1050 pub custom_buttons: Vec<Html>,
1051
1052 #[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 #[prop_or_default]
1062 pub frame_class: &'static str,
1063
1064 #[prop_or_default]
1066 pub style: &'static str,
1067
1068 #[prop_or_default]
1070 pub id: &'static str,
1071
1072 #[prop_or("Browser window")]
1076 pub aria_label: &'static str,
1077
1078 #[prop_or_default]
1080 pub aria_describedby: &'static str,
1081
1082 #[prop_or_default]
1084 pub container_class: &'static str,
1085
1086 #[prop_or("text-black dark:text-white")]
1088 pub input_class: &'static str,
1089
1090 #[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 #[prop_or("Refresh")]
1102 pub refresh_button_aria_label: &'static str,
1103
1104 #[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 #[prop_or("flex: 1; display: flex; justify-content: center; padding-right: 8px;")]
1116 pub address_wrapper_base_style: &'static str,
1117
1118 #[prop_or("display: flex; align-items: center; position: relative;")]
1122 pub header_base_style: &'static str,
1123
1124 #[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 #[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 #[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 #[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 #[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 #[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#[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}