1#![doc = include_str!("../DIOXUS.md")]
2
3use crate::common::{ButtonType, Size, Variant};
4use dioxus::prelude::*;
5use gloo_timers::callback::Timeout;
6use std::rc::Rc;
7use web_sys::{
8 HtmlInputElement,
9 wasm_bindgen::{JsCast, prelude::*},
10 window,
11};
12
13#[derive(PartialEq, Props, Clone)]
14pub struct BrowserContentProps {
15 #[props(default)]
16 pub class: &'static str,
17 #[props(default)]
18 pub style: &'static str,
19 #[props(default = "Browser content area")]
20 pub aria_label: &'static str,
21 #[props(default)]
22 pub aria_describedby: &'static str,
23 children: Element,
24}
25
26#[component]
27pub fn BrowserContent(props: BrowserContentProps) -> Element {
28 rsx! {
29 main {
30 class: "{props.class}",
31 style: "{props.style}",
32 role: "main",
33 aria_label: "{props.aria_label}",
34 aria_describedby: "{props.aria_describedby}",
35 tabindex: "-1",
36 {props.children}
37 }
38 }
39}
40
41#[derive(PartialEq, Props, Clone)]
42pub struct AddressBarProps {
43 #[props(default)]
44 pub url: String,
45 #[props(default = "Enter URL or search...")]
46 pub placeholder: &'static str,
47 #[props(default)]
48 pub on_url_change: EventHandler<FormEvent>,
49 #[props(default)]
50 pub read_only: bool,
51 #[props(default)]
52 pub class: &'static str,
53 #[props(
54 default = "flex: 1; margin-left: 1rem; margin-right: 1rem; border: 1px solid #d1d5db; border-radius: 0.375rem; padding-left: 0.75rem; padding-right: 0.75rem; font-size: 0.875rem; position: relative;"
55 )]
56 pub style: &'static str,
57 #[props(default = "Website address or search query")]
58 pub label: &'static str,
59 #[props(default = "Enter a website URL or search term. Press Enter to navigate.")]
60 pub describedby: &'static str,
61 #[props(default = "browser-url-input")]
62 pub input_id: &'static str,
63 #[props(default = "text-black dark:text-white")]
64 pub input_class: &'static str,
65 #[props(default)]
66 pub container_class: &'static str,
67 #[props(
68 default = "position: absolute; top: 50%; right: 8px; transform: translateY(-50%); padding: 4px; background: none; border: none; box-shadow: none; outline: none; cursor: pointer;"
69 )]
70 pub refresh_button_style: &'static str,
71 #[props(default = "Refresh")]
72 pub refresh_button_aria_label: &'static str,
73 #[props(
74 default = "background-color: transparent; padding-right: 2rem; border: none; outline: none; box-shadow: none; height: 100%;"
75 )]
76 pub input_style: &'static str,
77}
78
79#[component]
80pub fn AddressBar(props: AddressBarProps) -> Element {
81 let mut input_value = use_signal(|| props.url.clone());
82 let mut is_focused = use_signal(|| false);
83 let mut input_ref: Signal<Option<Rc<MountedData>>> = use_signal(|| None);
84
85 use_effect(move || {
86 input_value.set(props.url.clone());
87 });
88
89 let on_input_change = move |evt: FormEvent| {
90 input_value.set(evt.value());
91 props.on_url_change.call(evt);
92 };
93 let on_key_down = move |evt: Event<KeyboardData>| {
94 if evt.key() == Key::Enter {
95 evt.prevent_default();
96
97 if let Some(node) = &*input_ref.read() {
98 if let Some(input) = node.downcast::<HtmlInputElement>() {
99 let _ = input.blur();
100 }
101 }
102
103 if let Some(document) = window().and_then(|w| w.document()) {
104 let live_region = document.create_element("div").unwrap();
105 live_region.set_attribute("aria-live", "polite").unwrap();
106 live_region.set_attribute("aria-atomic", "true").unwrap();
107 live_region.set_class_name("sr-only");
108 live_region
109 .set_text_content(Some(&format!("Navigating to {}", input_value.read())));
110 document.body().unwrap().append_child(&live_region).unwrap();
111
112 let clone = live_region.clone();
113 Timeout::new(1000, move || {
114 let _ = document.body().unwrap().remove_child(&clone);
115 })
116 .forget();
117 }
118 }
119 };
120
121 rsx! {
122 div {
123 class: "{props.container_class} {props.class}",
124 style: "{props.style}",
125 label {
126 r#for: "{props.input_id}",
127 class: "sr-only",
128 "{props.label}"
129 }
130 input {
131 id: "{props.input_id}",
132 r#type: "text",
133 class: "{props.input_class}",
134 style: "{props.input_style}",
135 value: "{input_value}",
136 oninput: on_input_change,
137 onkeydown: on_key_down,
138 onfocus: move |_| is_focused.set(true),
139 onblur: move |_| is_focused.set(false),
140 placeholder: "{props.placeholder}",
141 readonly: props.read_only,
142 aria_describedby: "{props.describedby}",
143 autocomplete: "url",
144 spellcheck: "false",
145 onmounted: move |cx| input_ref.set(Some(cx.data())),
146 }
147 button {
148 style: "{props.refresh_button_style}",
149 aria_label: "{props.refresh_button_aria_label}",
150 onclick: move |_| {
151 let _ = window().unwrap().location().reload();
152 },
153 svg {
154 width: "11",
155 height: "13",
156 view_box: "0 0 11 13",
157 fill: "none",
158 xmlns: "http://www.w3.org/2000/svg",
159 path {
160 d: "M4.99385 1.00002L7.33006 3.33623L4.99385 5.67244M10 7.61925C10 10.1998 7.9081 12.2917 5.3276 12.2917C2.74709 12.2917 0.655182 10.1998 0.655182 7.61925C0.655182 5.03875 2.74709 2.94684 5.3276 2.94684C5.8737 2.94684 6.4957 2.94684 7.27443 3.33621",
161 stroke: "#767676",
162 stroke_linecap: "round",
163 stroke_linejoin: "round"
164 }
165 }
166 }
167 }
168 }
169}
170
171#[derive(Props, PartialEq, Clone)]
172pub struct ControlButtonProps {
173 pub r#type: ButtonType,
174 #[props(default)]
175 pub on_click: EventHandler<()>,
176 #[props(default)]
177 pub on_mouse_over: EventHandler<()>,
178 #[props(default)]
179 pub on_mouse_out: EventHandler<()>,
180 #[props(default)]
181 pub on_focus: EventHandler<FocusEvent>,
182 #[props(default)]
183 pub on_blur: EventHandler<FocusEvent>,
184 #[props(
185 default = "width: 1rem; height: 1rem; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; cursor: pointer; background: none; border: none; padding: 0; margin-right: 0.5rem;"
186 )]
187 pub style: &'static str,
188 #[props(default)]
189 pub class: &'static str,
190 #[props(default)]
191 pub svg_class: &'static str,
192 #[props(default)]
193 pub path_class: &'static str,
194 #[props(default = "button")]
195 pub button_type: &'static str,
196 #[props(default)]
197 pub aria_label: &'static str,
198 #[props(default)]
199 pub title: &'static str,
200 #[props(default = "0")]
201 pub tabindex: &'static str,
202}
203
204#[component]
205pub fn ControlButton(props: ControlButtonProps) -> Element {
206 let (fill, stroke) = match props.r#type {
207 ButtonType::Close => ("#FF5F57", "#E14640"),
208 ButtonType::Minimize => ("#FFBD2E", "#DFA123"),
209 ButtonType::Maximize => ("#28CA42", "#1DAD2C"),
210 };
211
212 let aria_label = if props.aria_label.is_empty() {
213 props.r#type.default_aria_label()
214 } else {
215 props.aria_label
216 };
217
218 let title = if props.title.is_empty() {
219 props.r#type.default_title()
220 } else {
221 props.title
222 };
223
224 rsx! {
225 button {
226 r#type: "{props.button_type}",
227 class: "{props.class}",
228 style: "{props.style}",
229 aria_label: "{aria_label}",
230 title: "{title}",
231 tabindex: "{props.tabindex}",
232 onclick: move |_| props.on_click.call(()),
233 onmouseover: move |_| props.on_mouse_over.call(()),
234 onmouseout: move |_| props.on_mouse_out.call(()),
235 onfocus: props.on_focus,
236 onblur: props.on_blur,
237
238 svg {
239 class: "{props.svg_class}",
240 width: "12",
241 height: "12",
242 view_box: "0 0 12 12",
243 fill: "none",
244 xmlns: "http://www.w3.org/2000/svg",
245 path {
246 class: "{props.path_class}",
247 d: "M6 0.5C9.03757 0.5 11.5 2.96243 11.5 6C11.5 9.03757 9.03757 11.5 6 11.5C2.96243 11.5 0.5 9.03757 0.5 6C0.5 2.96243 2.96243 0.5 6 0.5Z",
248 fill: "{fill}",
249 stroke: "{stroke}"
250 }
251 }
252 }
253 }
254}
255
256#[derive(PartialEq, Props, Clone)]
257pub struct BrowserControlsProps {
258 #[props(default)]
259 pub show_controls: bool,
260 #[props(default)]
261 pub class: &'static str,
262 #[props(default = "display: flex; align-items: center; background: none; padding-left: 10px;")]
263 pub style: &'static str,
264
265 #[props(default)]
266 pub on_close: EventHandler<()>,
267 #[props(default)]
268 pub on_close_mouse_over: EventHandler<()>,
269 #[props(default)]
270 pub on_close_mouse_out: EventHandler<()>,
271 #[props(default)]
272 pub on_close_focus: EventHandler<FocusEvent>,
273 #[props(default)]
274 pub on_close_blur: EventHandler<FocusEvent>,
275 #[props(default)]
276 pub close_class: &'static str,
277 #[props(default)]
278 pub close_svg_class: &'static str,
279 #[props(default)]
280 pub close_path_class: &'static str,
281 #[props(default = "button")]
282 pub close_button_type: &'static str,
283 #[props(default)]
284 pub close_aria_label: &'static str,
285 #[props(default)]
286 pub close_title: &'static str,
287 #[props(default = "0")]
288 pub close_tabindex: &'static str,
289
290 #[props(default)]
291 pub on_minimize: EventHandler<()>,
292 #[props(default)]
293 pub on_minimize_mouse_over: EventHandler<()>,
294 #[props(default)]
295 pub on_minimize_mouse_out: EventHandler<()>,
296 #[props(default)]
297 pub on_minimize_focus: EventHandler<FocusEvent>,
298 #[props(default)]
299 pub on_minimize_blur: EventHandler<FocusEvent>,
300 #[props(default)]
301 pub minimize_class: &'static str,
302 #[props(default)]
303 pub minimize_svg_class: &'static str,
304 #[props(default)]
305 pub minimize_path_class: &'static str,
306 #[props(default = "button")]
307 pub minimize_button_type: &'static str,
308 #[props(default)]
309 pub minimize_aria_label: &'static str,
310 #[props(default)]
311 pub minimize_title: &'static str,
312 #[props(default = "0")]
313 pub minimize_tabindex: &'static str,
314
315 #[props(default)]
316 pub on_maximize: EventHandler<()>,
317 #[props(default)]
318 pub on_maximize_mouse_over: EventHandler<()>,
319 #[props(default)]
320 pub on_maximize_mouse_out: EventHandler<()>,
321 #[props(default)]
322 pub on_maximize_focus: EventHandler<FocusEvent>,
323 #[props(default)]
324 pub on_maximize_blur: EventHandler<FocusEvent>,
325 #[props(default)]
326 pub maximize_class: &'static str,
327 #[props(default)]
328 pub maximize_svg_class: &'static str,
329 #[props(default)]
330 pub maximize_path_class: &'static str,
331 #[props(default = "button")]
332 pub maximize_button_type: &'static str,
333 #[props(default)]
334 pub maximize_aria_label: &'static str,
335 #[props(default)]
336 pub maximize_title: &'static str,
337 #[props(default = "0")]
338 pub maximize_tabindex: &'static str,
339}
340
341#[component]
342pub fn BrowserControls(props: BrowserControlsProps) -> Element {
343 if !props.show_controls {
344 return rsx! {};
345 }
346
347 rsx! {
348 nav {
349 class: "{props.class}",
350 style: "{props.style}",
351 role: "toolbar",
352 aria_label: "Browser window controls",
353 ControlButton {
354 r#type: ButtonType::Close,
355 on_click: props.on_close,
356 on_mouse_over: props.on_close_mouse_over,
357 on_mouse_out: props.on_close_mouse_out,
358 on_focus: props.on_close_focus,
359 on_blur: props.on_close_blur,
360 class: props.close_class,
361 svg_class: props.close_svg_class,
362 path_class: props.close_path_class,
363 button_type: props.close_button_type,
364 aria_label: props.close_aria_label,
365 title: props.close_title,
366 tabindex: props.close_tabindex,
367 }
368 ControlButton {
369 r#type: ButtonType::Minimize,
370 on_click: props.on_minimize,
371 on_mouse_over: props.on_minimize_mouse_over,
372 on_mouse_out: props.on_minimize_mouse_out,
373 on_focus: props.on_minimize_focus,
374 on_blur: props.on_minimize_blur,
375 class: props.minimize_class,
376 svg_class: props.minimize_svg_class,
377 path_class: props.minimize_path_class,
378 button_type: props.minimize_button_type,
379 aria_label: props.minimize_aria_label,
380 title: props.minimize_title,
381 tabindex: props.minimize_tabindex,
382 }
383 ControlButton {
384 r#type: ButtonType::Maximize,
385 on_click: props.on_maximize,
386 on_mouse_over: props.on_maximize_mouse_over,
387 on_mouse_out: props.on_maximize_mouse_out,
388 on_focus: props.on_maximize_focus,
389 on_blur: props.on_maximize_blur,
390 class: props.maximize_class,
391 svg_class: props.maximize_svg_class,
392 path_class: props.maximize_path_class,
393 button_type: props.maximize_button_type,
394 aria_label: props.maximize_aria_label,
395 title: props.maximize_title,
396 tabindex: props.maximize_tabindex,
397 }
398 }
399 }
400}
401
402#[derive(PartialEq, Props, Clone)]
403pub struct BrowserHeaderProps {
404 #[props(default)]
405 pub url: String,
406 #[props(default)]
407 pub placeholder: &'static str,
408 #[props(default)]
409 pub on_url_change: Option<EventHandler<FormEvent>>,
410 #[props(default = true)]
411 pub show_controls: bool,
412 #[props(default = true)]
413 pub show_address_bar: bool,
414 #[props(default = false)]
415 pub read_only: bool,
416 #[props(default)]
417 pub variant: Variant,
418 #[props(default)]
419 pub size: Size,
420 #[props(default)]
421 pub custom_buttons: Vec<Element>,
422 #[props(default)]
423 pub class: &'static str,
424
425 #[props(default)]
426 pub container_class: &'static str,
427 #[props(default = "text-black dark:text-white")]
428 pub input_class: &'static str,
429 #[props(default)]
430 pub refresh_button_style: &'static str,
431 #[props(default = "Refresh")]
432 pub refresh_button_aria_label: &'static str,
433
434 #[props(
435 default = "padding: 4px; cursor: pointer; background: none; border: none; box-shadow: none; outline: none;"
436 )]
437 pub icon_button_style: &'static str,
438
439 #[props(default = "flex: 1; display: flex; justify-content: center; padding-right: 8px;")]
440 pub address_wrapper_base_style: &'static str,
441
442 #[props(default = "display: flex; align-items: center; position: relative;")]
443 pub header_base_style: &'static str,
444
445 #[props(default)]
446 pub on_close: EventHandler<()>,
447 #[props(default)]
448 pub on_close_mouse_over: EventHandler<()>,
449 #[props(default)]
450 pub on_close_mouse_out: EventHandler<()>,
451 #[props(default)]
452 pub on_close_focus: EventHandler<FocusEvent>,
453 #[props(default)]
454 pub on_close_blur: EventHandler<FocusEvent>,
455 #[props(default)]
456 pub close_class: &'static str,
457 #[props(default)]
458 pub close_svg_class: &'static str,
459 #[props(default)]
460 pub close_path_class: &'static str,
461 #[props(default = "button")]
462 pub close_button_type: &'static str,
463 #[props(default)]
464 pub close_aria_label: &'static str,
465 #[props(default)]
466 pub close_title: &'static str,
467 #[props(default = "0")]
468 pub close_tabindex: &'static str,
469
470 #[props(default)]
471 pub on_minimize: EventHandler<()>,
472 #[props(default)]
473 pub on_minimize_mouse_over: EventHandler<()>,
474 #[props(default)]
475 pub on_minimize_mouse_out: EventHandler<()>,
476 #[props(default)]
477 pub on_minimize_focus: EventHandler<FocusEvent>,
478 #[props(default)]
479 pub on_minimize_blur: EventHandler<FocusEvent>,
480 #[props(default)]
481 pub minimize_class: &'static str,
482 #[props(default)]
483 pub minimize_svg_class: &'static str,
484 #[props(default)]
485 pub minimize_path_class: &'static str,
486 #[props(default = "button")]
487 pub minimize_button_type: &'static str,
488 #[props(default)]
489 pub minimize_aria_label: &'static str,
490 #[props(default)]
491 pub minimize_title: &'static str,
492 #[props(default = "0")]
493 pub minimize_tabindex: &'static str,
494
495 #[props(default)]
496 pub on_maximize: EventHandler<()>,
497 #[props(default)]
498 pub on_maximize_mouse_over: EventHandler<()>,
499 #[props(default)]
500 pub on_maximize_mouse_out: EventHandler<()>,
501 #[props(default)]
502 pub on_maximize_focus: EventHandler<FocusEvent>,
503 #[props(default)]
504 pub on_maximize_blur: EventHandler<FocusEvent>,
505 #[props(default)]
506 pub maximize_class: &'static str,
507 #[props(default)]
508 pub maximize_svg_class: &'static str,
509 #[props(default)]
510 pub maximize_path_class: &'static str,
511 #[props(default = "button")]
512 pub maximize_button_type: &'static str,
513 #[props(default)]
514 pub maximize_aria_label: &'static str,
515 #[props(default)]
516 pub maximize_title: &'static str,
517 #[props(default = "0")]
518 pub maximize_tabindex: &'static str,
519
520 #[props(default)]
521 pub share_button_style: &'static str,
522 #[props(default)]
523 pub share_onclick: EventHandler<()>,
524 #[props(default)]
525 pub share_onmouseover: EventHandler<()>,
526 #[props(default)]
527 pub share_onmouseout: EventHandler<()>,
528 #[props(default)]
529 pub share_onfocus: EventHandler<FocusEvent>,
530 #[props(default)]
531 pub share_onblur: EventHandler<FocusEvent>,
532 #[props(default)]
533 pub share_tabindex: &'static str,
534
535 #[props(default)]
536 pub tabs_button_style: &'static str,
537 #[props(default)]
538 pub tabs_onclick: EventHandler<()>,
539 #[props(default)]
540 pub tabs_onmouseover: EventHandler<()>,
541 #[props(default)]
542 pub tabs_onmouseout: EventHandler<()>,
543 #[props(default)]
544 pub tabs_onfocus: EventHandler<FocusEvent>,
545 #[props(default)]
546 pub tabs_onblur: EventHandler<FocusEvent>,
547 #[props(default)]
548 pub tabs_tabindex: &'static str,
549
550 #[props(default)]
551 pub more_button_style: &'static str,
552 #[props(default)]
553 pub more_onclick: EventHandler<()>,
554 #[props(default)]
555 pub more_onmouseover: EventHandler<()>,
556 #[props(default)]
557 pub more_onmouseout: EventHandler<()>,
558 #[props(default)]
559 pub more_onfocus: EventHandler<FocusEvent>,
560 #[props(default)]
561 pub more_onblur: EventHandler<FocusEvent>,
562 #[props(default)]
563 pub more_tabindex: &'static str,
564}
565
566#[component]
567pub fn BrowserHeader(props: BrowserHeaderProps) -> Element {
568 let is_ios = props.variant == Variant::Ios;
569 let is_tabs = props.variant == Variant::Tabs;
570
571 let base_style = {
572 let padding = match props.size {
573 Size::Small => "4px 6px",
574 Size::Large => "10px 16px",
575 _ => "6px 12px",
576 };
577 let height = match (props.variant.clone(), props.size.clone()) {
578 (Variant::Tabs, _) => "40px",
579 (Variant::Ios, _) => "56px",
580 (_, Size::Large) => "60px",
581 (_, Size::Small) => "38px",
582 _ => "48px",
583 };
584 let border_radius = if is_tabs {
585 "6px"
586 } else if props.variant == Variant::Default {
587 "8px 8px 0 0"
588 } else {
589 "0"
590 };
591 let border = if is_tabs { "1px solid #d1d5db" } else { "none" };
592 let box_shadow = if props.variant == Variant::Default {
593 "0 2px 6px rgba(0,0,0,0.1)"
594 } else {
595 "none"
596 };
597
598 format!(
599 "{} justify-content: {}; padding: {}; height: {}; border-radius: {}; border: {}; box-shadow: {};",
600 props.header_base_style,
601 if is_ios {
602 "space-between"
603 } else {
604 "flex-start"
605 },
606 padding,
607 height,
608 border_radius,
609 border,
610 box_shadow
611 )
612 };
613
614 let address_wrapper_style = format!(
615 "{} padding-left: {};",
616 props.address_wrapper_base_style,
617 if props.show_controls { "8px" } else { "0" }
618 );
619
620 let share_onclick = move |_| props.share_onclick.call(());
621 let share_onmouseover = move |_| props.share_onmouseover.call(());
622 let share_onmouseout = move |_| props.share_onmouseout.call(());
623
624 let tabs_onclick = move |_| props.tabs_onclick.call(());
625 let tabs_onmouseover = move |_| props.tabs_onmouseover.call(());
626 let tabs_onmouseout = move |_| props.tabs_onmouseout.call(());
627
628 let more_onclick = move |_| props.more_onclick.call(());
629 let more_onmouseover = move |_| props.more_onmouseover.call(());
630 let more_onmouseout = move |_| props.more_onmouseout.call(());
631
632 rsx! {
633 header {
634 style: "{base_style}",
635 class: "{props.class}",
636 "aria-label": "Browser window header",
637
638 div {
639 style: "display: flex; align-items: center; gap: 6px;",
640 if props.show_controls {
641 BrowserControls {
642 on_close: props.on_close,
643 on_minimize: props.on_minimize,
644 on_maximize: props.on_maximize,
645 show_controls: props.show_controls,
646 on_close_mouse_over: props.on_close_mouse_over,
647 on_close_mouse_out: props.on_close_mouse_out,
648 on_close_focus: props.on_close_focus,
649 on_close_blur: props.on_close_blur,
650 close_class: props.close_class,
651 close_svg_class: props.close_svg_class,
652 close_path_class: props.close_path_class,
653 close_button_type: props.close_button_type,
654 close_aria_label: props.close_aria_label,
655 close_title: props.close_title,
656 close_tabindex: props.close_tabindex,
657 on_minimize_mouse_over: props.on_minimize_mouse_over,
658 on_minimize_mouse_out: props.on_minimize_mouse_out,
659 on_minimize_focus: props.on_minimize_focus,
660 on_minimize_blur: props.on_minimize_blur,
661 minimize_class: props.minimize_class,
662 minimize_svg_class: props.minimize_svg_class,
663 minimize_path_class: props.minimize_path_class,
664 minimize_button_type: props.minimize_button_type,
665 minimize_aria_label: props.minimize_aria_label,
666 minimize_title: props.minimize_title,
667 minimize_tabindex: props.minimize_tabindex,
668 on_maximize_mouse_over: props.on_maximize_mouse_over,
669 on_maximize_mouse_out: props.on_maximize_mouse_out,
670 on_maximize_focus: props.on_maximize_focus,
671 on_maximize_blur: props.on_maximize_blur,
672 maximize_class: props.maximize_class,
673 maximize_svg_class: props.maximize_svg_class,
674 maximize_path_class: props.maximize_path_class,
675 maximize_button_type: props.maximize_button_type,
676 maximize_aria_label: props.maximize_aria_label,
677 maximize_title: props.maximize_title,
678 maximize_tabindex: props.maximize_tabindex,
679 }
680 if !is_ios {
681 button {
682 style: "{props.icon_button_style}",
683 "aria-label": "Sidebar",
684 svg {
685 width: "20",
686 height: "15",
687 view_box: "0 0 20 15",
688 fill: "none",
689 xmlns: "http://www.w3.org/2000/svg",
690 path {
691 d: "M2.62346 15H16.4609C18.2202 15 19.0844 14.1358 19.0844 12.4074V2.59259C19.0844 0.864204 18.2202 0 16.4609 0H2.62346C0.874483 0 0 0.864204 0 2.59259V12.4074C0 14.1358 0.874483 15 2.62346 15ZM2.64404 13.5082C1.90329 13.5082 1.48149 13.1173 1.48149 12.3354V2.66461C1.48149 1.89301 1.90329 1.49177 2.64404 1.49177H6.22427V13.5082H2.64404ZM16.4403 1.49177C17.1811 1.49177 17.6029 1.89301 17.6029 2.66461V12.3354C17.6029 13.1173 17.1811 13.5082 16.4403 13.5082H7.67489V1.49177H16.4403ZM4.67078 4.47532C4.94857 4.47532 5.18518 4.2284 5.18518 3.9609C5.18518 3.69341 4.94857 3.46708 4.67078 3.46708H3.05556C2.78806 3.46708 2.55144 3.69341 2.55144 3.9609C2.55144 4.2284 2.78806 4.47532 3.05556 4.47532H4.67078ZM4.67078 6.53293C4.94857 6.53293 5.18518 6.29629 5.18518 6.01853C5.18518 5.75102 4.94857 5.52469 4.67078 5.52469H3.05556C2.78806 5.52469 2.55144 5.75102 2.55144 6.01853C2.55144 6.29629 2.78806 6.53293 3.05556 6.53293H4.67078ZM4.67078 8.59054C4.94857 8.59054 5.18518 8.35392 5.18518 8.08642C5.18518 7.81893 4.94857 7.5926 4.67078 7.5926H3.05556C2.78806 7.5926 2.55144 7.81893 2.55144 8.08642C2.55144 8.35392 2.78806 8.59054 3.05556 8.59054H4.67078Z",
692 fill: "#767676",
693 }
694 }
695 }
696 button {
697 style: "{props.icon_button_style}",
698 "aria-label": "Back",
699 svg {
700 width: "9",
701 height: "16",
702 view_box: "0 0 9 16",
703 fill: "none",
704 xmlns: "http://www.w3.org/2000/svg",
705 path {
706 d: "M7.5 1.5L1 8L7.5 14.5",
707 stroke: "#737373",
708 stroke_width: "1.5",
709 stroke_linecap: "round",
710 stroke_linejoin: "round",
711 }
712 }
713 }
714 button {
715 style: "{props.icon_button_style}",
716 "aria-label": "Forward",
717 svg {
718 width: "9",
719 height: "16",
720 view_box: "0 0 9 16",
721 fill: "none",
722 xmlns: "http://www.w3.org/2000/svg",
723 path {
724 d: "M1 14.5L7.5 8L1 1.5",
725 stroke: "#BFBFBF",
726 stroke_width: "1.5",
727 stroke_linecap: "round",
728 stroke_linejoin: "round",
729 }
730 }
731 }
732 }
733 }
734 }
735
736 if props.show_address_bar {
737 div {
738 style: "{address_wrapper_style}",
739 AddressBar {
740 url: props.url,
741 placeholder: props.placeholder,
742 on_url_change: props.on_url_change.unwrap_or_default(),
743 read_only: props.read_only,
744 input_class: props.input_class,
745 container_class: props.container_class,
746 refresh_button_style: props.refresh_button_style,
747 refresh_button_aria_label: props.refresh_button_aria_label,
748 }
749 }
750 }
751
752 div {
753 style: "display: flex; align-items: center; gap: 6px; margin-left: auto;",
754 if props.show_controls {
755 for btn in &props.custom_buttons {
756 {btn}
757 }
758 button {
759 style: "{props.icon_button_style}",
760 onclick: share_onclick,
761 onmouseover: share_onmouseover,
762 onmouseout: share_onmouseout,
763 onfocus: props.share_onfocus,
764 onblur: props.share_onblur,
765 "aria-label": "Share",
766 title: "Share",
767 tabindex: "{props.share_tabindex}",
768 svg {
769 width: "15",
770 height: "19",
771 view_box: "0 0 15 19",
772 fill: "none",
773 xmlns: "http://www.w3.org/2000/svg",
774 path {
775 d: "M7.49467 12.3969C7.91045 12.3969 8.26225 12.056 8.26225 11.6513V3.34416L8.1983 2.06613L8.64605 2.55604L9.81876 3.82343C9.95736 3.97254 10.1493 4.04709 10.3305 4.04709C10.7356 4.04709 11.0341 3.77017 11.0341 3.38676C11.0341 3.17377 10.9488 3.02467 10.7996 2.88621L8.04905 0.255589C7.85715 0.0638861 7.69722 0 7.49467 0C7.30277 0 7.14286 0.0638861 6.94029 0.255589L4.18977 2.88621C4.05117 3.02467 3.96589 3.17377 3.96589 3.38676C3.96589 3.77017 4.25372 4.04709 4.65885 4.04709C4.84009 4.04709 5.04264 3.97254 5.18124 3.82343L6.35395 2.55604L6.80171 2.06613L6.73774 3.34416V11.6513C6.73774 12.056 7.08955 12.3969 7.49467 12.3969ZM2.71855 19H12.2814C14.1045 19 15 18.1054 15 16.3161V8.12611C15 6.33688 14.1045 5.44225 12.2814 5.44225H9.98934V6.98654H12.2601C13.0171 6.98654 13.4648 7.4019 13.4648 8.20066V16.2416C13.4648 17.051 13.0171 17.4557 12.2601 17.4557H2.73988C1.97228 17.4557 1.53519 17.051 1.53519 16.2416V8.20066C1.53519 7.4019 1.97228 6.98654 2.73988 6.98654H5.01065V5.44225H2.71855C0.906181 5.44225 0 6.33688 0 8.12611V16.3161C0 18.1054 0.906181 19 2.71855 19Z",
776 fill: "#767676",
777 }
778 }
779 }
780 button {
781 style: "{props.icon_button_style}",
782 onclick: tabs_onclick,
783 onmouseover: tabs_onmouseover,
784 onmouseout: tabs_onmouseout,
785 onfocus: props.tabs_onfocus,
786 onblur: props.tabs_onblur,
787 "aria-label": "Tabs",
788 title: "Tabs",
789 tabindex: "{props.tabs_tabindex}",
790 svg {
791 width: "15",
792 height: "15",
793 view_box: "0 0 15 15",
794 fill: "none",
795 xmlns: "http://www.w3.org/2000/svg",
796 path {
797 d: "M7.01662 14.6401C7.4887 14.6401 7.87493 14.2646 7.87493 13.7925V8.3745H13.1642C13.6255 8.3745 14.0225 7.97755 14.0225 7.50547C14.0225 7.03341 13.6255 6.63643 13.1642 6.63643H7.87493V1.20768C7.87493 0.735619 7.4887 0.360107 7.01662 0.360107C6.54456 0.360107 6.14758 0.735619 6.14758 1.20768V6.63643H0.869031C0.396973 6.63643 0 7.03341 0 7.50547C0 7.97755 0.396973 8.3745 0.869031 8.3745H6.14758V13.7925C6.14758 14.2646 6.54456 14.6401 7.01662 14.6401Z",
798 fill: "#767676",
799 }
800 }
801 }
802 button {
803 style: "{props.icon_button_style}",
804 onclick: more_onclick,
805 onmouseover: more_onmouseover,
806 onmouseout: more_onmouseout,
807 onfocus: props.more_onfocus,
808 onblur: props.more_onblur,
809 "aria-label": "More options",
810 title: "More options",
811 tabindex: "{props.more_tabindex}",
812 svg {
813 width: "18",
814 height: "19",
815 view_box: "0 0 18 19",
816 fill: "none",
817 xmlns: "http://www.w3.org/2000/svg",
818 path {
819 d: "M2.67776 14.2898H3.97934V15.5914C3.97934 17.3407 4.85401 18.205 6.63458 18.205H14.8189C16.5891 18.205 17.4742 17.3407 17.4742 15.5914V7.32373C17.4742 5.5744 16.5891 4.71016 14.8189 4.71016H13.5174V3.40857C13.5174 1.65923 12.6323 0.794983 10.8621 0.794983H2.67776C0.897191 0.794983 0.022522 1.65923 0.022522 3.40857V11.6762C0.022522 13.4256 0.897191 14.2898 2.67776 14.2898ZM2.69859 12.7904C1.94886 12.7904 1.52195 12.3843 1.52195 11.5929V3.49187C1.52195 2.70051 1.94886 2.29442 2.69859 2.29442H10.8413C11.591 2.29442 12.0179 2.70051 12.0179 3.49187V4.71016H6.63458C4.85401 4.71016 3.97934 5.5744 3.97934 7.32373V12.7904H2.69859ZM6.65539 16.7056C5.90568 16.7056 5.47878 16.2995 5.47878 15.5081V7.40704C5.47878 6.61567 5.90568 6.20957 6.65539 6.20957H14.7981C15.5478 6.20957 15.9747 6.61567 15.9747 7.40704V15.5081C15.9747 16.2995 15.5478 16.7056 14.7981 16.7056H6.65539Z",
820 fill: "#767676",
821 }
822 }
823 }
824 }
825 }
826 }
827 }
828}
829
830#[derive(Clone, PartialEq, Props)]
831pub struct KeyboardNavigationOptions {
832 pub on_escape: Option<EventHandler<()>>,
833 pub on_enter: Option<EventHandler<()>>,
834 pub trap_focus: bool,
835}
836
837pub fn use_keyboard(options: KeyboardNavigationOptions) -> Signal<Option<Rc<MountedData>>> {
838 let container_ref: Signal<Option<Rc<MountedData>>> = use_signal(|| None);
839
840 {
841 let options = options.clone();
842
843 use_effect(move || {
844 let closure = Closure::<dyn Fn(web_sys::KeyboardEvent)>::wrap(Box::new(
845 move |event: web_sys::KeyboardEvent| {
846 let key = event.key();
847 let target = event.target();
848
849 match key.as_str() {
850 "Escape" => {
851 if let Some(callback) = &options.on_escape {
852 event.prevent_default();
853 callback.call(());
854 }
855 }
856 "Enter" => {
857 if let Some(callback) = &options.on_enter {
858 if let Some(_target_elem) = target
859 .clone()
860 .and_then(|t| t.dyn_into::<web_sys::Element>().ok())
861 {
862 if let Some(container) = container_ref
863 .read()
864 .as_ref()
865 .and_then(|r| r.downcast::<web_sys::Element>())
866 {
867 if target.unwrap() == ***container {
868 event.prevent_default();
869 callback.call(());
870 }
871 }
872 }
873 }
874 }
875 "Tab" if options.trap_focus => {
876 if let Some(container) = container_ref
877 .read()
878 .as_ref()
879 .and_then(|r| r.downcast::<web_sys::Element>())
880 {
881 let selector = "button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])";
882 let focusables = container.query_selector_all(selector).unwrap();
883
884 let length = focusables.length();
885 if length == 0 {
886 return;
887 }
888
889 let first = focusables
890 .item(0)
891 .and_then(|e| e.dyn_into::<web_sys::HtmlElement>().ok());
892 let last = focusables
893 .item(length - 1)
894 .and_then(|e| e.dyn_into::<web_sys::HtmlElement>().ok());
895
896 let document = web_sys::window().unwrap().document().unwrap();
897 let active = document.active_element();
898
899 if event.shift_key() {
900 if active == first.as_ref().map(|e| e.clone().into()) {
901 event.prevent_default();
902 if let Some(elem) = last {
903 elem.focus().ok();
904 }
905 }
906 } else if active == last.as_ref().map(|e| e.clone().into()) {
907 event.prevent_default();
908 if let Some(elem) = first {
909 elem.focus().ok();
910 }
911 }
912 }
913 }
914 _ => {}
915 }
916 },
917 )
918 as Box<dyn Fn(web_sys::KeyboardEvent)>);
919
920 web_sys::window()
921 .unwrap()
922 .add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref())
923 .unwrap();
924 });
925 }
926
927 container_ref
928}
929#[derive(PartialEq, Props, Clone)]
934pub struct BrowserFrameProps {
935 #[props(default)]
937 pub children: Element,
938
939 #[props(default)]
941 pub url: String,
942
943 #[props(default)]
945 pub placeholder: &'static str,
946
947 #[props(default)]
949 pub on_url_change: Option<EventHandler<FormEvent>>,
950
951 #[props(default)]
953 pub on_close: EventHandler<()>,
954
955 #[props(default)]
957 pub on_minimize: EventHandler<()>,
958
959 #[props(default)]
961 pub on_maximize: EventHandler<()>,
962
963 #[props(default = true)]
967 pub show_controls: bool,
968
969 #[props(default = true)]
973 pub show_address_bar: bool,
974
975 #[props(default = false)]
979 pub read_only: bool,
980
981 #[props(default)]
983 pub size: Size,
984
985 #[props(default)]
987 pub variant: Variant,
988
989 #[props(default)]
991 pub custom_buttons: Vec<Element>,
992
993 #[props(
998 default = "rounded-lg border shadow-lg overflow-hidden bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700"
999 )]
1000 pub class: &'static str,
1001
1002 #[props(default)]
1004 pub frame_class: &'static str,
1005
1006 #[props(default)]
1008 pub style: &'static str,
1009
1010 #[props(default)]
1012 pub id: &'static str,
1013
1014 #[props(default = "Browser window")]
1018 pub aria_label: &'static str,
1019
1020 #[props(default)]
1022 pub aria_describedby: &'static str,
1023
1024 #[props(default)]
1026 pub container_class: &'static str,
1027
1028 #[props(default = "text-black dark:text-white")]
1032 pub input_class: &'static str,
1033
1034 #[props(
1036 default = "position: absolute; top: 50%; right: 8px; transform: translateY(-50%); padding: 4px; background: none; border: none; box-shadow: none; outline: none; cursor: pointer;"
1037 )]
1038 pub refresh_button_style: &'static str,
1039
1040 #[props(default = "Refresh")]
1044 pub refresh_button_aria_label: &'static str,
1045
1046 #[props(
1048 default = "padding: 4px; cursor: pointer; background: none; border: none; box-shadow: none; outline: none;"
1049 )]
1050 pub icon_button_style: &'static str,
1051
1052 #[props(default = "flex: 1; display: flex; justify-content: center; padding-right: 8px;")]
1054 pub address_wrapper_base_style: &'static str,
1055
1056 #[props(default = "display: flex; align-items: center; position: relative;")]
1058 pub header_base_style: &'static str,
1059
1060 #[props(default)]
1062 pub on_close_mouse_over: EventHandler<()>,
1063 #[props(default)]
1064 pub on_close_mouse_out: EventHandler<()>,
1065 #[props(default)]
1066 pub on_close_focus: EventHandler<FocusEvent>,
1067 #[props(default)]
1068 pub on_close_blur: EventHandler<FocusEvent>,
1069 #[props(default)]
1070 pub close_class: &'static str,
1071 #[props(default)]
1072 pub close_svg_class: &'static str,
1073 #[props(default)]
1074 pub close_path_class: &'static str,
1075 #[props(default = "button")]
1076 pub close_button_type: &'static str,
1077 #[props(default)]
1078 pub close_aria_label: &'static str,
1079 #[props(default)]
1080 pub close_title: &'static str,
1081 #[props(default = "0")]
1082 pub close_tabindex: &'static str,
1083
1084 #[props(default)]
1086 pub on_minimize_mouse_over: EventHandler<()>,
1087 #[props(default)]
1088 pub on_minimize_mouse_out: EventHandler<()>,
1089 #[props(default)]
1090 pub on_minimize_focus: EventHandler<FocusEvent>,
1091 #[props(default)]
1092 pub on_minimize_blur: EventHandler<FocusEvent>,
1093 #[props(default)]
1094 pub minimize_class: &'static str,
1095 #[props(default)]
1096 pub minimize_svg_class: &'static str,
1097 #[props(default)]
1098 pub minimize_path_class: &'static str,
1099 #[props(default = "button")]
1100 pub minimize_button_type: &'static str,
1101 #[props(default)]
1102 pub minimize_aria_label: &'static str,
1103 #[props(default)]
1104 pub minimize_title: &'static str,
1105 #[props(default = "0")]
1106 pub minimize_tabindex: &'static str,
1107
1108 #[props(default)]
1110 pub on_maximize_mouse_over: EventHandler<()>,
1111 #[props(default)]
1112 pub on_maximize_mouse_out: EventHandler<()>,
1113 #[props(default)]
1114 pub on_maximize_focus: EventHandler<FocusEvent>,
1115 #[props(default)]
1116 pub on_maximize_blur: EventHandler<FocusEvent>,
1117 #[props(default)]
1118 pub maximize_class: &'static str,
1119 #[props(default)]
1120 pub maximize_svg_class: &'static str,
1121 #[props(default)]
1122 pub maximize_path_class: &'static str,
1123 #[props(default = "button")]
1124 pub maximize_button_type: &'static str,
1125 #[props(default)]
1126 pub maximize_aria_label: &'static str,
1127 #[props(default)]
1128 pub maximize_title: &'static str,
1129 #[props(default = "0")]
1130 pub maximize_tabindex: &'static str,
1131
1132 #[props(default)]
1134 pub share_button_style: &'static str,
1135 #[props(default)]
1136 pub share_onclick: EventHandler<()>,
1137 #[props(default)]
1138 pub share_onmouseover: EventHandler<()>,
1139 #[props(default)]
1140 pub share_onmouseout: EventHandler<()>,
1141 #[props(default)]
1142 pub share_onfocus: EventHandler<FocusEvent>,
1143 #[props(default)]
1144 pub share_onblur: EventHandler<FocusEvent>,
1145 #[props(default)]
1146 pub share_tabindex: &'static str,
1147
1148 #[props(default)]
1150 pub tabs_button_style: &'static str,
1151 #[props(default)]
1152 pub tabs_onclick: EventHandler<()>,
1153 #[props(default)]
1154 pub tabs_onmouseover: EventHandler<()>,
1155 #[props(default)]
1156 pub tabs_onmouseout: EventHandler<()>,
1157 #[props(default)]
1158 pub tabs_onfocus: EventHandler<FocusEvent>,
1159 #[props(default)]
1160 pub tabs_onblur: EventHandler<FocusEvent>,
1161 #[props(default)]
1162 pub tabs_tabindex: &'static str,
1163
1164 #[props(default)]
1166 pub more_button_style: &'static str,
1167 #[props(default)]
1168 pub more_onclick: EventHandler<()>,
1169 #[props(default)]
1170 pub more_onmouseover: EventHandler<()>,
1171 #[props(default)]
1172 pub more_onmouseout: EventHandler<()>,
1173 #[props(default)]
1174 pub more_onfocus: EventHandler<FocusEvent>,
1175 #[props(default)]
1176 pub more_onblur: EventHandler<FocusEvent>,
1177 #[props(default)]
1178 pub more_tabindex: &'static str,
1179}
1180
1181#[component]
1270pub fn BrowserFrame(props: BrowserFrameProps) -> Element {
1271 let on_close = props.on_close;
1272
1273 let mut container_ref = use_keyboard(KeyboardNavigationOptions {
1274 on_escape: Some(EventHandler::new(move |_| {
1275 on_close.call(());
1276 })),
1277 on_enter: None,
1278 trap_focus: false,
1279 });
1280
1281 let size_style = props.size.to_style();
1282 let combined_style = format!("{} {}", size_style, props.style);
1283
1284 rsx! {
1285 article {
1286 id: "{props.id}",
1287 class: "{props.class}",
1288 style: "{combined_style}",
1289 role: "application",
1290 aria_label: "{props.aria_label}",
1291 aria_describedby: "{props.aria_describedby}",
1292 tabindex: "-1",
1293 onmounted: move |cx| container_ref.set(Some(cx.data())),
1294
1295 BrowserHeader {
1296 url: props.url,
1297 placeholder: props.placeholder,
1298 on_url_change: props.on_url_change,
1299 on_close: props.on_close,
1300 on_minimize: props.on_minimize,
1301 on_maximize: props.on_maximize,
1302 show_controls: props.show_controls,
1303 show_address_bar: props.show_address_bar,
1304 read_only: props.read_only,
1305 variant: props.variant,
1306 size: props.size,
1307 custom_buttons: props.custom_buttons,
1308 class: props.frame_class,
1309 container_class: props.container_class,
1310 input_class: props.input_class,
1311 refresh_button_style: props.refresh_button_style,
1312 refresh_button_aria_label: props.refresh_button_aria_label,
1313 icon_button_style: props.icon_button_style,
1314 address_wrapper_base_style: props.address_wrapper_base_style,
1315 header_base_style: props.header_base_style,
1316 on_close_mouse_over: props.on_close_mouse_over,
1317 on_close_mouse_out: props.on_close_mouse_out,
1318 on_close_focus: props.on_close_focus,
1319 on_close_blur: props.on_close_blur,
1320 close_class: props.close_class,
1321 close_svg_class: props.close_svg_class,
1322 close_path_class: props.close_path_class,
1323 close_button_type: props.close_button_type,
1324 close_aria_label: props.close_aria_label,
1325 close_title: props.close_title,
1326 close_tabindex: props.close_tabindex,
1327 on_minimize_mouse_over: props.on_minimize_mouse_over,
1328 on_minimize_mouse_out: props.on_minimize_mouse_out,
1329 on_minimize_focus: props.on_minimize_focus,
1330 on_minimize_blur: props.on_minimize_blur,
1331 minimize_class: props.minimize_class,
1332 minimize_svg_class: props.minimize_svg_class,
1333 minimize_path_class: props.minimize_path_class,
1334 minimize_button_type: props.minimize_button_type,
1335 minimize_aria_label: props.minimize_aria_label,
1336 minimize_title: props.minimize_title,
1337 minimize_tabindex: props.minimize_tabindex,
1338 on_maximize_mouse_over: props.on_maximize_mouse_over,
1339 on_maximize_mouse_out: props.on_maximize_mouse_out,
1340 on_maximize_focus: props.on_maximize_focus,
1341 on_maximize_blur: props.on_maximize_blur,
1342 maximize_class: props.maximize_class,
1343 maximize_svg_class: props.maximize_svg_class,
1344 maximize_path_class: props.maximize_path_class,
1345 maximize_button_type: props.maximize_button_type,
1346 maximize_aria_label: props.maximize_aria_label,
1347 maximize_title: props.maximize_title,
1348 maximize_tabindex: props.maximize_tabindex,
1349 share_button_style: props.share_button_style,
1350 share_onclick: props.share_onclick,
1351 share_onmouseover: props.share_onmouseover,
1352 share_onmouseout: props.share_onmouseout,
1353 share_onfocus: props.share_onfocus,
1354 share_onblur: props.share_onblur,
1355 share_tabindex: props.share_tabindex,
1356 tabs_button_style: props.tabs_button_style,
1357 tabs_onclick: props.tabs_onclick,
1358 tabs_onmouseover: props.tabs_onmouseover,
1359 tabs_onmouseout: props.tabs_onmouseout,
1360 tabs_onfocus: props.tabs_onfocus,
1361 tabs_onblur: props.tabs_onblur,
1362 tabs_tabindex: props.tabs_tabindex,
1363 more_button_style: props.more_button_style,
1364 more_onclick: props.more_onclick,
1365 more_onmouseover: props.more_onmouseover,
1366 more_onmouseout: props.more_onmouseout,
1367 more_onfocus: props.more_onfocus,
1368 more_onblur: props.more_onblur,
1369 more_tabindex: props.more_tabindex,
1370 }
1371 BrowserContent {
1372 aria_describedby: props.aria_describedby,
1373 {props.children}
1374 }
1375 }
1376 }
1377}