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.set_attribute("style", "position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;").unwrap();
109 live_region
110 .set_text_content(Some(&format!("Navigating to {}", input_value.read())));
111 document.body().unwrap().append_child(&live_region).unwrap();
112
113 let clone = live_region.clone();
114 Timeout::new(1000, move || {
115 let _ = document.body().unwrap().remove_child(&clone);
116 })
117 .forget();
118 }
119 }
120 };
121
122 rsx! {
123 div {
124 class: "{props.container_class} {props.class}",
125 style: "{props.style}",
126 label {
127 r#for: "{props.input_id}",
128 class: "sr-only",
129 style: "position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;",
130 "{props.label}"
131 }
132 input {
133 id: "{props.input_id}",
134 r#type: "text",
135 class: "{props.input_class}",
136 style: "{props.input_style}",
137 value: "{input_value}",
138 oninput: on_input_change,
139 onkeydown: on_key_down,
140 onfocus: move |_| is_focused.set(true),
141 onblur: move |_| is_focused.set(false),
142 placeholder: "{props.placeholder}",
143 readonly: props.read_only,
144 aria_describedby: "{props.describedby}",
145 autocomplete: "url",
146 spellcheck: "false",
147 onmounted: move |cx| input_ref.set(Some(cx.data())),
148 }
149 button {
150 style: "{props.refresh_button_style}",
151 aria_label: "{props.refresh_button_aria_label}",
152 onclick: move |_| {
153 let _ = window().unwrap().location().reload();
154 },
155 svg {
156 width: "11",
157 height: "13",
158 view_box: "0 0 11 13",
159 fill: "none",
160 xmlns: "http://www.w3.org/2000/svg",
161 path {
162 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",
163 stroke: "#767676",
164 stroke_linecap: "round",
165 stroke_linejoin: "round"
166 }
167 }
168 }
169 }
170 }
171}
172
173#[derive(Props, PartialEq, Clone)]
174pub struct ControlButtonProps {
175 pub r#type: ButtonType,
176 #[props(default)]
177 pub on_click: EventHandler<()>,
178 #[props(default)]
179 pub on_mouse_over: EventHandler<()>,
180 #[props(default)]
181 pub on_mouse_out: EventHandler<()>,
182 #[props(default)]
183 pub on_focus: EventHandler<FocusEvent>,
184 #[props(default)]
185 pub on_blur: EventHandler<FocusEvent>,
186 #[props(
187 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;"
188 )]
189 pub style: &'static str,
190 #[props(default)]
191 pub class: &'static str,
192 #[props(default)]
193 pub svg_class: &'static str,
194 #[props(default)]
195 pub path_class: &'static str,
196 #[props(default = "button")]
197 pub button_type: &'static str,
198 #[props(default)]
199 pub aria_label: &'static str,
200 #[props(default)]
201 pub title: &'static str,
202 #[props(default = "0")]
203 pub tabindex: &'static str,
204}
205
206#[component]
207pub fn ControlButton(props: ControlButtonProps) -> Element {
208 let (fill, stroke) = match props.r#type {
209 ButtonType::Close => ("#FF5F57", "#E14640"),
210 ButtonType::Minimize => ("#FFBD2E", "#DFA123"),
211 ButtonType::Maximize => ("#28CA42", "#1DAD2C"),
212 };
213
214 let aria_label = if props.aria_label.is_empty() {
215 props.r#type.default_aria_label()
216 } else {
217 props.aria_label
218 };
219
220 let title = if props.title.is_empty() {
221 props.r#type.default_title()
222 } else {
223 props.title
224 };
225
226 rsx! {
227 button {
228 r#type: "{props.button_type}",
229 class: "{props.class}",
230 style: "{props.style}",
231 aria_label: "{aria_label}",
232 title: "{title}",
233 tabindex: "{props.tabindex}",
234 onclick: move |_| props.on_click.call(()),
235 onmouseover: move |_| props.on_mouse_over.call(()),
236 onmouseout: move |_| props.on_mouse_out.call(()),
237 onfocus: props.on_focus,
238 onblur: props.on_blur,
239
240 svg {
241 class: "{props.svg_class}",
242 width: "12",
243 height: "12",
244 view_box: "0 0 12 12",
245 fill: "none",
246 xmlns: "http://www.w3.org/2000/svg",
247 path {
248 class: "{props.path_class}",
249 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",
250 fill: "{fill}",
251 stroke: "{stroke}"
252 }
253 }
254 }
255 }
256}
257
258#[derive(PartialEq, Props, Clone)]
259pub struct BrowserControlsProps {
260 #[props(default)]
261 pub show_controls: bool,
262 #[props(default)]
263 pub class: &'static str,
264 #[props(default = "display: flex; align-items: center; background: none; padding-left: 10px;")]
265 pub style: &'static str,
266
267 #[props(default)]
268 pub on_close: EventHandler<()>,
269 #[props(default)]
270 pub on_close_mouse_over: EventHandler<()>,
271 #[props(default)]
272 pub on_close_mouse_out: EventHandler<()>,
273 #[props(default)]
274 pub on_close_focus: EventHandler<FocusEvent>,
275 #[props(default)]
276 pub on_close_blur: EventHandler<FocusEvent>,
277 #[props(default)]
278 pub close_class: &'static str,
279 #[props(default)]
280 pub close_svg_class: &'static str,
281 #[props(default)]
282 pub close_path_class: &'static str,
283 #[props(default = "button")]
284 pub close_button_type: &'static str,
285 #[props(default)]
286 pub close_aria_label: &'static str,
287 #[props(default)]
288 pub close_title: &'static str,
289 #[props(default = "0")]
290 pub close_tabindex: &'static str,
291
292 #[props(default)]
293 pub on_minimize: EventHandler<()>,
294 #[props(default)]
295 pub on_minimize_mouse_over: EventHandler<()>,
296 #[props(default)]
297 pub on_minimize_mouse_out: EventHandler<()>,
298 #[props(default)]
299 pub on_minimize_focus: EventHandler<FocusEvent>,
300 #[props(default)]
301 pub on_minimize_blur: EventHandler<FocusEvent>,
302 #[props(default)]
303 pub minimize_class: &'static str,
304 #[props(default)]
305 pub minimize_svg_class: &'static str,
306 #[props(default)]
307 pub minimize_path_class: &'static str,
308 #[props(default = "button")]
309 pub minimize_button_type: &'static str,
310 #[props(default)]
311 pub minimize_aria_label: &'static str,
312 #[props(default)]
313 pub minimize_title: &'static str,
314 #[props(default = "0")]
315 pub minimize_tabindex: &'static str,
316
317 #[props(default)]
318 pub on_maximize: EventHandler<()>,
319 #[props(default)]
320 pub on_maximize_mouse_over: EventHandler<()>,
321 #[props(default)]
322 pub on_maximize_mouse_out: EventHandler<()>,
323 #[props(default)]
324 pub on_maximize_focus: EventHandler<FocusEvent>,
325 #[props(default)]
326 pub on_maximize_blur: EventHandler<FocusEvent>,
327 #[props(default)]
328 pub maximize_class: &'static str,
329 #[props(default)]
330 pub maximize_svg_class: &'static str,
331 #[props(default)]
332 pub maximize_path_class: &'static str,
333 #[props(default = "button")]
334 pub maximize_button_type: &'static str,
335 #[props(default)]
336 pub maximize_aria_label: &'static str,
337 #[props(default)]
338 pub maximize_title: &'static str,
339 #[props(default = "0")]
340 pub maximize_tabindex: &'static str,
341}
342
343#[component]
344pub fn BrowserControls(props: BrowserControlsProps) -> Element {
345 if !props.show_controls {
346 return rsx! {};
347 }
348
349 rsx! {
350 nav {
351 class: "{props.class}",
352 style: "{props.style}",
353 role: "toolbar",
354 aria_label: "Browser window controls",
355 ControlButton {
356 r#type: ButtonType::Close,
357 on_click: props.on_close,
358 on_mouse_over: props.on_close_mouse_over,
359 on_mouse_out: props.on_close_mouse_out,
360 on_focus: props.on_close_focus,
361 on_blur: props.on_close_blur,
362 class: props.close_class,
363 svg_class: props.close_svg_class,
364 path_class: props.close_path_class,
365 button_type: props.close_button_type,
366 aria_label: props.close_aria_label,
367 title: props.close_title,
368 tabindex: props.close_tabindex,
369 }
370 ControlButton {
371 r#type: ButtonType::Minimize,
372 on_click: props.on_minimize,
373 on_mouse_over: props.on_minimize_mouse_over,
374 on_mouse_out: props.on_minimize_mouse_out,
375 on_focus: props.on_minimize_focus,
376 on_blur: props.on_minimize_blur,
377 class: props.minimize_class,
378 svg_class: props.minimize_svg_class,
379 path_class: props.minimize_path_class,
380 button_type: props.minimize_button_type,
381 aria_label: props.minimize_aria_label,
382 title: props.minimize_title,
383 tabindex: props.minimize_tabindex,
384 }
385 ControlButton {
386 r#type: ButtonType::Maximize,
387 on_click: props.on_maximize,
388 on_mouse_over: props.on_maximize_mouse_over,
389 on_mouse_out: props.on_maximize_mouse_out,
390 on_focus: props.on_maximize_focus,
391 on_blur: props.on_maximize_blur,
392 class: props.maximize_class,
393 svg_class: props.maximize_svg_class,
394 path_class: props.maximize_path_class,
395 button_type: props.maximize_button_type,
396 aria_label: props.maximize_aria_label,
397 title: props.maximize_title,
398 tabindex: props.maximize_tabindex,
399 }
400 }
401 }
402}
403
404#[derive(PartialEq, Props, Clone)]
405pub struct BrowserHeaderProps {
406 #[props(default)]
407 pub url: String,
408 #[props(default)]
409 pub placeholder: &'static str,
410 #[props(default)]
411 pub on_url_change: Option<EventHandler<FormEvent>>,
412 #[props(default = true)]
413 pub show_controls: bool,
414 #[props(default = true)]
415 pub show_address_bar: bool,
416 #[props(default = false)]
417 pub read_only: bool,
418 #[props(default)]
419 pub variant: Variant,
420 #[props(default)]
421 pub size: Size,
422 #[props(default)]
423 pub custom_buttons: Vec<Element>,
424 #[props(default)]
425 pub class: &'static str,
426
427 #[props(default)]
428 pub container_class: &'static str,
429 #[props(default = "text-black dark:text-white")]
430 pub input_class: &'static str,
431 #[props(default)]
432 pub refresh_button_style: &'static str,
433 #[props(default = "Refresh")]
434 pub refresh_button_aria_label: &'static str,
435
436 #[props(
437 default = "padding: 4px; cursor: pointer; background: none; border: none; box-shadow: none; outline: none;"
438 )]
439 pub icon_button_style: &'static str,
440
441 #[props(default = "flex: 1; display: flex; justify-content: center; padding-right: 8px;")]
442 pub address_wrapper_base_style: &'static str,
443
444 #[props(default = "display: flex; align-items: center; position: relative;")]
445 pub header_base_style: &'static str,
446
447 #[props(default)]
448 pub on_close: EventHandler<()>,
449 #[props(default)]
450 pub on_close_mouse_over: EventHandler<()>,
451 #[props(default)]
452 pub on_close_mouse_out: EventHandler<()>,
453 #[props(default)]
454 pub on_close_focus: EventHandler<FocusEvent>,
455 #[props(default)]
456 pub on_close_blur: EventHandler<FocusEvent>,
457 #[props(default)]
458 pub close_class: &'static str,
459 #[props(default)]
460 pub close_svg_class: &'static str,
461 #[props(default)]
462 pub close_path_class: &'static str,
463 #[props(default = "button")]
464 pub close_button_type: &'static str,
465 #[props(default)]
466 pub close_aria_label: &'static str,
467 #[props(default)]
468 pub close_title: &'static str,
469 #[props(default = "0")]
470 pub close_tabindex: &'static str,
471
472 #[props(default)]
473 pub on_minimize: EventHandler<()>,
474 #[props(default)]
475 pub on_minimize_mouse_over: EventHandler<()>,
476 #[props(default)]
477 pub on_minimize_mouse_out: EventHandler<()>,
478 #[props(default)]
479 pub on_minimize_focus: EventHandler<FocusEvent>,
480 #[props(default)]
481 pub on_minimize_blur: EventHandler<FocusEvent>,
482 #[props(default)]
483 pub minimize_class: &'static str,
484 #[props(default)]
485 pub minimize_svg_class: &'static str,
486 #[props(default)]
487 pub minimize_path_class: &'static str,
488 #[props(default = "button")]
489 pub minimize_button_type: &'static str,
490 #[props(default)]
491 pub minimize_aria_label: &'static str,
492 #[props(default)]
493 pub minimize_title: &'static str,
494 #[props(default = "0")]
495 pub minimize_tabindex: &'static str,
496
497 #[props(default)]
498 pub on_maximize: EventHandler<()>,
499 #[props(default)]
500 pub on_maximize_mouse_over: EventHandler<()>,
501 #[props(default)]
502 pub on_maximize_mouse_out: EventHandler<()>,
503 #[props(default)]
504 pub on_maximize_focus: EventHandler<FocusEvent>,
505 #[props(default)]
506 pub on_maximize_blur: EventHandler<FocusEvent>,
507 #[props(default)]
508 pub maximize_class: &'static str,
509 #[props(default)]
510 pub maximize_svg_class: &'static str,
511 #[props(default)]
512 pub maximize_path_class: &'static str,
513 #[props(default = "button")]
514 pub maximize_button_type: &'static str,
515 #[props(default)]
516 pub maximize_aria_label: &'static str,
517 #[props(default)]
518 pub maximize_title: &'static str,
519 #[props(default = "0")]
520 pub maximize_tabindex: &'static str,
521
522 #[props(default)]
523 pub share_button_style: &'static str,
524 #[props(default)]
525 pub share_onclick: EventHandler<()>,
526 #[props(default)]
527 pub share_onmouseover: EventHandler<()>,
528 #[props(default)]
529 pub share_onmouseout: EventHandler<()>,
530 #[props(default)]
531 pub share_onfocus: EventHandler<FocusEvent>,
532 #[props(default)]
533 pub share_onblur: EventHandler<FocusEvent>,
534 #[props(default)]
535 pub share_tabindex: &'static str,
536
537 #[props(default)]
538 pub tabs_button_style: &'static str,
539 #[props(default)]
540 pub tabs_onclick: EventHandler<()>,
541 #[props(default)]
542 pub tabs_onmouseover: EventHandler<()>,
543 #[props(default)]
544 pub tabs_onmouseout: EventHandler<()>,
545 #[props(default)]
546 pub tabs_onfocus: EventHandler<FocusEvent>,
547 #[props(default)]
548 pub tabs_onblur: EventHandler<FocusEvent>,
549 #[props(default)]
550 pub tabs_tabindex: &'static str,
551
552 #[props(default)]
553 pub more_button_style: &'static str,
554 #[props(default)]
555 pub more_onclick: EventHandler<()>,
556 #[props(default)]
557 pub more_onmouseover: EventHandler<()>,
558 #[props(default)]
559 pub more_onmouseout: EventHandler<()>,
560 #[props(default)]
561 pub more_onfocus: EventHandler<FocusEvent>,
562 #[props(default)]
563 pub more_onblur: EventHandler<FocusEvent>,
564 #[props(default)]
565 pub more_tabindex: &'static str,
566}
567
568#[component]
569pub fn BrowserHeader(props: BrowserHeaderProps) -> Element {
570 let is_ios = props.variant == Variant::Ios;
571 let is_tabs = props.variant == Variant::Tabs;
572
573 let base_style = {
574 let padding = match props.size {
575 Size::Small => "4px 6px",
576 Size::Large => "10px 16px",
577 _ => "6px 12px",
578 };
579 let height = match (props.variant.clone(), props.size.clone()) {
580 (Variant::Tabs, _) => "40px",
581 (Variant::Ios, _) => "56px",
582 (_, Size::Large) => "60px",
583 (_, Size::Small) => "38px",
584 _ => "48px",
585 };
586 let border_radius = if is_tabs {
587 "6px"
588 } else if props.variant == Variant::Default {
589 "8px 8px 0 0"
590 } else {
591 "0"
592 };
593 let border = if is_tabs { "1px solid #d1d5db" } else { "none" };
594 let box_shadow = if props.variant == Variant::Default {
595 "0 2px 6px rgba(0,0,0,0.1)"
596 } else {
597 "none"
598 };
599
600 format!(
601 "{} justify-content: {}; padding: {}; height: {}; border-radius: {}; border: {}; box-shadow: {};",
602 props.header_base_style,
603 if is_ios {
604 "space-between"
605 } else {
606 "flex-start"
607 },
608 padding,
609 height,
610 border_radius,
611 border,
612 box_shadow
613 )
614 };
615
616 let address_wrapper_style = format!(
617 "{} padding-left: {};",
618 props.address_wrapper_base_style,
619 if props.show_controls { "8px" } else { "0" }
620 );
621
622 let share_onclick = move |_| props.share_onclick.call(());
623 let share_onmouseover = move |_| props.share_onmouseover.call(());
624 let share_onmouseout = move |_| props.share_onmouseout.call(());
625
626 let tabs_onclick = move |_| props.tabs_onclick.call(());
627 let tabs_onmouseover = move |_| props.tabs_onmouseover.call(());
628 let tabs_onmouseout = move |_| props.tabs_onmouseout.call(());
629
630 let more_onclick = move |_| props.more_onclick.call(());
631 let more_onmouseover = move |_| props.more_onmouseover.call(());
632 let more_onmouseout = move |_| props.more_onmouseout.call(());
633
634 rsx! {
635 header {
636 style: "{base_style}",
637 class: "{props.class}",
638 "aria-label": "Browser window header",
639
640 div {
641 style: "display: flex; align-items: center; gap: 6px;",
642 if props.show_controls {
643 BrowserControls {
644 on_close: props.on_close,
645 on_minimize: props.on_minimize,
646 on_maximize: props.on_maximize,
647 show_controls: props.show_controls,
648 on_close_mouse_over: props.on_close_mouse_over,
649 on_close_mouse_out: props.on_close_mouse_out,
650 on_close_focus: props.on_close_focus,
651 on_close_blur: props.on_close_blur,
652 close_class: props.close_class,
653 close_svg_class: props.close_svg_class,
654 close_path_class: props.close_path_class,
655 close_button_type: props.close_button_type,
656 close_aria_label: props.close_aria_label,
657 close_title: props.close_title,
658 close_tabindex: props.close_tabindex,
659 on_minimize_mouse_over: props.on_minimize_mouse_over,
660 on_minimize_mouse_out: props.on_minimize_mouse_out,
661 on_minimize_focus: props.on_minimize_focus,
662 on_minimize_blur: props.on_minimize_blur,
663 minimize_class: props.minimize_class,
664 minimize_svg_class: props.minimize_svg_class,
665 minimize_path_class: props.minimize_path_class,
666 minimize_button_type: props.minimize_button_type,
667 minimize_aria_label: props.minimize_aria_label,
668 minimize_title: props.minimize_title,
669 minimize_tabindex: props.minimize_tabindex,
670 on_maximize_mouse_over: props.on_maximize_mouse_over,
671 on_maximize_mouse_out: props.on_maximize_mouse_out,
672 on_maximize_focus: props.on_maximize_focus,
673 on_maximize_blur: props.on_maximize_blur,
674 maximize_class: props.maximize_class,
675 maximize_svg_class: props.maximize_svg_class,
676 maximize_path_class: props.maximize_path_class,
677 maximize_button_type: props.maximize_button_type,
678 maximize_aria_label: props.maximize_aria_label,
679 maximize_title: props.maximize_title,
680 maximize_tabindex: props.maximize_tabindex,
681 }
682 if !is_ios {
683 button {
684 style: "{props.icon_button_style}",
685 "aria-label": "Sidebar",
686 svg {
687 width: "20",
688 height: "15",
689 view_box: "0 0 20 15",
690 fill: "none",
691 xmlns: "http://www.w3.org/2000/svg",
692 path {
693 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",
694 fill: "#767676",
695 }
696 }
697 }
698 button {
699 style: "{props.icon_button_style}",
700 "aria-label": "Back",
701 svg {
702 width: "9",
703 height: "16",
704 view_box: "0 0 9 16",
705 fill: "none",
706 xmlns: "http://www.w3.org/2000/svg",
707 path {
708 d: "M7.5 1.5L1 8L7.5 14.5",
709 stroke: "#737373",
710 stroke_width: "1.5",
711 stroke_linecap: "round",
712 stroke_linejoin: "round",
713 }
714 }
715 }
716 button {
717 style: "{props.icon_button_style}",
718 "aria-label": "Forward",
719 svg {
720 width: "9",
721 height: "16",
722 view_box: "0 0 9 16",
723 fill: "none",
724 xmlns: "http://www.w3.org/2000/svg",
725 path {
726 d: "M1 14.5L7.5 8L1 1.5",
727 stroke: "#BFBFBF",
728 stroke_width: "1.5",
729 stroke_linecap: "round",
730 stroke_linejoin: "round",
731 }
732 }
733 }
734 }
735 }
736 }
737
738 if props.show_address_bar {
739 div {
740 style: "{address_wrapper_style}",
741 AddressBar {
742 url: props.url,
743 placeholder: props.placeholder,
744 on_url_change: props.on_url_change.unwrap_or_default(),
745 read_only: props.read_only,
746 input_class: props.input_class,
747 container_class: props.container_class,
748 refresh_button_style: props.refresh_button_style,
749 refresh_button_aria_label: props.refresh_button_aria_label,
750 }
751 }
752 }
753
754 div {
755 style: "display: flex; align-items: center; gap: 6px; margin-left: auto;",
756 if props.show_controls {
757 for btn in &props.custom_buttons {
758 {btn}
759 }
760 button {
761 style: "{props.icon_button_style}",
762 onclick: share_onclick,
763 onmouseover: share_onmouseover,
764 onmouseout: share_onmouseout,
765 onfocus: props.share_onfocus,
766 onblur: props.share_onblur,
767 "aria-label": "Share",
768 title: "Share",
769 tabindex: "{props.share_tabindex}",
770 svg {
771 width: "15",
772 height: "19",
773 view_box: "0 0 15 19",
774 fill: "none",
775 xmlns: "http://www.w3.org/2000/svg",
776 path {
777 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",
778 fill: "#767676",
779 }
780 }
781 }
782 button {
783 style: "{props.icon_button_style}",
784 onclick: tabs_onclick,
785 onmouseover: tabs_onmouseover,
786 onmouseout: tabs_onmouseout,
787 onfocus: props.tabs_onfocus,
788 onblur: props.tabs_onblur,
789 "aria-label": "Tabs",
790 title: "Tabs",
791 tabindex: "{props.tabs_tabindex}",
792 svg {
793 width: "15",
794 height: "15",
795 view_box: "0 0 15 15",
796 fill: "none",
797 xmlns: "http://www.w3.org/2000/svg",
798 path {
799 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",
800 fill: "#767676",
801 }
802 }
803 }
804 button {
805 style: "{props.icon_button_style}",
806 onclick: more_onclick,
807 onmouseover: more_onmouseover,
808 onmouseout: more_onmouseout,
809 onfocus: props.more_onfocus,
810 onblur: props.more_onblur,
811 "aria-label": "More options",
812 title: "More options",
813 tabindex: "{props.more_tabindex}",
814 svg {
815 width: "18",
816 height: "19",
817 view_box: "0 0 18 19",
818 fill: "none",
819 xmlns: "http://www.w3.org/2000/svg",
820 path {
821 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",
822 fill: "#767676",
823 }
824 }
825 }
826 }
827 }
828 }
829 }
830}
831
832#[derive(Clone, PartialEq, Props)]
833pub struct KeyboardNavigationOptions {
834 pub on_escape: Option<EventHandler<()>>,
835 pub on_enter: Option<EventHandler<()>>,
836 pub trap_focus: bool,
837}
838
839pub fn use_keyboard(options: KeyboardNavigationOptions) -> Signal<Option<Rc<MountedData>>> {
840 let container_ref: Signal<Option<Rc<MountedData>>> = use_signal(|| None);
841
842 {
843 let options = options.clone();
844
845 use_effect(move || {
846 let closure = Closure::<dyn Fn(web_sys::KeyboardEvent)>::wrap(Box::new(
847 move |event: web_sys::KeyboardEvent| {
848 let key = event.key();
849 let target = event.target();
850
851 match key.as_str() {
852 "Escape" => {
853 if let Some(callback) = &options.on_escape {
854 event.prevent_default();
855 callback.call(());
856 }
857 }
858 "Enter" => {
859 if let Some(callback) = &options.on_enter {
860 if let Some(_target_elem) = target
861 .clone()
862 .and_then(|t| t.dyn_into::<web_sys::Element>().ok())
863 {
864 if let Some(container) = container_ref
865 .read()
866 .as_ref()
867 .and_then(|r| r.downcast::<web_sys::Element>())
868 {
869 if target.unwrap() == ***container {
870 event.prevent_default();
871 callback.call(());
872 }
873 }
874 }
875 }
876 }
877 "Tab" if options.trap_focus => {
878 if let Some(container) = container_ref
879 .read()
880 .as_ref()
881 .and_then(|r| r.downcast::<web_sys::Element>())
882 {
883 let selector = "button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])";
884 let focusables = container.query_selector_all(selector).unwrap();
885
886 let length = focusables.length();
887 if length == 0 {
888 return;
889 }
890
891 let first = focusables
892 .item(0)
893 .and_then(|e| e.dyn_into::<web_sys::HtmlElement>().ok());
894 let last = focusables
895 .item(length - 1)
896 .and_then(|e| e.dyn_into::<web_sys::HtmlElement>().ok());
897
898 let document = web_sys::window().unwrap().document().unwrap();
899 let active = document.active_element();
900
901 if event.shift_key() {
902 if active == first.as_ref().map(|e| e.clone().into()) {
903 event.prevent_default();
904 if let Some(elem) = last {
905 elem.focus().ok();
906 }
907 }
908 } else if active == last.as_ref().map(|e| e.clone().into()) {
909 event.prevent_default();
910 if let Some(elem) = first {
911 elem.focus().ok();
912 }
913 }
914 }
915 }
916 _ => {}
917 }
918 },
919 )
920 as Box<dyn Fn(web_sys::KeyboardEvent)>);
921
922 web_sys::window()
923 .unwrap()
924 .add_event_listener_with_callback("keydown", closure.as_ref().unchecked_ref())
925 .unwrap();
926 });
927 }
928
929 container_ref
930}
931#[derive(PartialEq, Props, Clone)]
936pub struct BrowserFrameProps {
937 #[props(default)]
939 pub children: Element,
940
941 #[props(default)]
943 pub url: String,
944
945 #[props(default)]
947 pub placeholder: &'static str,
948
949 #[props(default)]
951 pub on_url_change: Option<EventHandler<FormEvent>>,
952
953 #[props(default)]
955 pub on_close: EventHandler<()>,
956
957 #[props(default)]
959 pub on_minimize: EventHandler<()>,
960
961 #[props(default)]
963 pub on_maximize: EventHandler<()>,
964
965 #[props(default = true)]
969 pub show_controls: bool,
970
971 #[props(default = true)]
975 pub show_address_bar: bool,
976
977 #[props(default = false)]
981 pub read_only: bool,
982
983 #[props(default)]
985 pub size: Size,
986
987 #[props(default)]
989 pub variant: Variant,
990
991 #[props(default)]
993 pub custom_buttons: Vec<Element>,
994
995 #[props(
1000 default = "rounded-lg border shadow-lg overflow-hidden bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700"
1001 )]
1002 pub class: &'static str,
1003
1004 #[props(default)]
1006 pub frame_class: &'static str,
1007
1008 #[props(default)]
1010 pub style: &'static str,
1011
1012 #[props(default)]
1014 pub id: &'static str,
1015
1016 #[props(default = "Browser window")]
1020 pub aria_label: &'static str,
1021
1022 #[props(default)]
1024 pub aria_describedby: &'static str,
1025
1026 #[props(default)]
1028 pub container_class: &'static str,
1029
1030 #[props(default = "text-black dark:text-white")]
1034 pub input_class: &'static str,
1035
1036 #[props(
1038 default = "position: absolute; top: 50%; right: 8px; transform: translateY(-50%); padding: 4px; background: none; border: none; box-shadow: none; outline: none; cursor: pointer;"
1039 )]
1040 pub refresh_button_style: &'static str,
1041
1042 #[props(default = "Refresh")]
1046 pub refresh_button_aria_label: &'static str,
1047
1048 #[props(
1050 default = "padding: 4px; cursor: pointer; background: none; border: none; box-shadow: none; outline: none;"
1051 )]
1052 pub icon_button_style: &'static str,
1053
1054 #[props(default = "flex: 1; display: flex; justify-content: center; padding-right: 8px;")]
1056 pub address_wrapper_base_style: &'static str,
1057
1058 #[props(default = "display: flex; align-items: center; position: relative;")]
1060 pub header_base_style: &'static str,
1061
1062 #[props(default)]
1064 pub on_close_mouse_over: EventHandler<()>,
1065 #[props(default)]
1066 pub on_close_mouse_out: EventHandler<()>,
1067 #[props(default)]
1068 pub on_close_focus: EventHandler<FocusEvent>,
1069 #[props(default)]
1070 pub on_close_blur: EventHandler<FocusEvent>,
1071 #[props(default)]
1072 pub close_class: &'static str,
1073 #[props(default)]
1074 pub close_svg_class: &'static str,
1075 #[props(default)]
1076 pub close_path_class: &'static str,
1077 #[props(default = "button")]
1078 pub close_button_type: &'static str,
1079 #[props(default)]
1080 pub close_aria_label: &'static str,
1081 #[props(default)]
1082 pub close_title: &'static str,
1083 #[props(default = "0")]
1084 pub close_tabindex: &'static str,
1085
1086 #[props(default)]
1088 pub on_minimize_mouse_over: EventHandler<()>,
1089 #[props(default)]
1090 pub on_minimize_mouse_out: EventHandler<()>,
1091 #[props(default)]
1092 pub on_minimize_focus: EventHandler<FocusEvent>,
1093 #[props(default)]
1094 pub on_minimize_blur: EventHandler<FocusEvent>,
1095 #[props(default)]
1096 pub minimize_class: &'static str,
1097 #[props(default)]
1098 pub minimize_svg_class: &'static str,
1099 #[props(default)]
1100 pub minimize_path_class: &'static str,
1101 #[props(default = "button")]
1102 pub minimize_button_type: &'static str,
1103 #[props(default)]
1104 pub minimize_aria_label: &'static str,
1105 #[props(default)]
1106 pub minimize_title: &'static str,
1107 #[props(default = "0")]
1108 pub minimize_tabindex: &'static str,
1109
1110 #[props(default)]
1112 pub on_maximize_mouse_over: EventHandler<()>,
1113 #[props(default)]
1114 pub on_maximize_mouse_out: EventHandler<()>,
1115 #[props(default)]
1116 pub on_maximize_focus: EventHandler<FocusEvent>,
1117 #[props(default)]
1118 pub on_maximize_blur: EventHandler<FocusEvent>,
1119 #[props(default)]
1120 pub maximize_class: &'static str,
1121 #[props(default)]
1122 pub maximize_svg_class: &'static str,
1123 #[props(default)]
1124 pub maximize_path_class: &'static str,
1125 #[props(default = "button")]
1126 pub maximize_button_type: &'static str,
1127 #[props(default)]
1128 pub maximize_aria_label: &'static str,
1129 #[props(default)]
1130 pub maximize_title: &'static str,
1131 #[props(default = "0")]
1132 pub maximize_tabindex: &'static str,
1133
1134 #[props(default)]
1136 pub share_button_style: &'static str,
1137 #[props(default)]
1138 pub share_onclick: EventHandler<()>,
1139 #[props(default)]
1140 pub share_onmouseover: EventHandler<()>,
1141 #[props(default)]
1142 pub share_onmouseout: EventHandler<()>,
1143 #[props(default)]
1144 pub share_onfocus: EventHandler<FocusEvent>,
1145 #[props(default)]
1146 pub share_onblur: EventHandler<FocusEvent>,
1147 #[props(default)]
1148 pub share_tabindex: &'static str,
1149
1150 #[props(default)]
1152 pub tabs_button_style: &'static str,
1153 #[props(default)]
1154 pub tabs_onclick: EventHandler<()>,
1155 #[props(default)]
1156 pub tabs_onmouseover: EventHandler<()>,
1157 #[props(default)]
1158 pub tabs_onmouseout: EventHandler<()>,
1159 #[props(default)]
1160 pub tabs_onfocus: EventHandler<FocusEvent>,
1161 #[props(default)]
1162 pub tabs_onblur: EventHandler<FocusEvent>,
1163 #[props(default)]
1164 pub tabs_tabindex: &'static str,
1165
1166 #[props(default)]
1168 pub more_button_style: &'static str,
1169 #[props(default)]
1170 pub more_onclick: EventHandler<()>,
1171 #[props(default)]
1172 pub more_onmouseover: EventHandler<()>,
1173 #[props(default)]
1174 pub more_onmouseout: EventHandler<()>,
1175 #[props(default)]
1176 pub more_onfocus: EventHandler<FocusEvent>,
1177 #[props(default)]
1178 pub more_onblur: EventHandler<FocusEvent>,
1179 #[props(default)]
1180 pub more_tabindex: &'static str,
1181}
1182
1183#[component]
1272pub fn BrowserFrame(props: BrowserFrameProps) -> Element {
1273 let on_close = props.on_close;
1274
1275 let mut container_ref = use_keyboard(KeyboardNavigationOptions {
1276 on_escape: Some(EventHandler::new(move |_| {
1277 on_close.call(());
1278 })),
1279 on_enter: None,
1280 trap_focus: false,
1281 });
1282
1283 let size_style = props.size.to_style();
1284 let combined_style = format!("{} {}", size_style, props.style);
1285
1286 rsx! {
1287 article {
1288 id: "{props.id}",
1289 class: "{props.class}",
1290 style: "{combined_style}",
1291 role: "application",
1292 aria_label: "{props.aria_label}",
1293 aria_describedby: "{props.aria_describedby}",
1294 tabindex: "-1",
1295 onmounted: move |cx| container_ref.set(Some(cx.data())),
1296
1297 BrowserHeader {
1298 url: props.url,
1299 placeholder: props.placeholder,
1300 on_url_change: props.on_url_change,
1301 on_close: props.on_close,
1302 on_minimize: props.on_minimize,
1303 on_maximize: props.on_maximize,
1304 show_controls: props.show_controls,
1305 show_address_bar: props.show_address_bar,
1306 read_only: props.read_only,
1307 variant: props.variant,
1308 size: props.size,
1309 custom_buttons: props.custom_buttons,
1310 class: props.frame_class,
1311 container_class: props.container_class,
1312 input_class: props.input_class,
1313 refresh_button_style: props.refresh_button_style,
1314 refresh_button_aria_label: props.refresh_button_aria_label,
1315 icon_button_style: props.icon_button_style,
1316 address_wrapper_base_style: props.address_wrapper_base_style,
1317 header_base_style: props.header_base_style,
1318 on_close_mouse_over: props.on_close_mouse_over,
1319 on_close_mouse_out: props.on_close_mouse_out,
1320 on_close_focus: props.on_close_focus,
1321 on_close_blur: props.on_close_blur,
1322 close_class: props.close_class,
1323 close_svg_class: props.close_svg_class,
1324 close_path_class: props.close_path_class,
1325 close_button_type: props.close_button_type,
1326 close_aria_label: props.close_aria_label,
1327 close_title: props.close_title,
1328 close_tabindex: props.close_tabindex,
1329 on_minimize_mouse_over: props.on_minimize_mouse_over,
1330 on_minimize_mouse_out: props.on_minimize_mouse_out,
1331 on_minimize_focus: props.on_minimize_focus,
1332 on_minimize_blur: props.on_minimize_blur,
1333 minimize_class: props.minimize_class,
1334 minimize_svg_class: props.minimize_svg_class,
1335 minimize_path_class: props.minimize_path_class,
1336 minimize_button_type: props.minimize_button_type,
1337 minimize_aria_label: props.minimize_aria_label,
1338 minimize_title: props.minimize_title,
1339 minimize_tabindex: props.minimize_tabindex,
1340 on_maximize_mouse_over: props.on_maximize_mouse_over,
1341 on_maximize_mouse_out: props.on_maximize_mouse_out,
1342 on_maximize_focus: props.on_maximize_focus,
1343 on_maximize_blur: props.on_maximize_blur,
1344 maximize_class: props.maximize_class,
1345 maximize_svg_class: props.maximize_svg_class,
1346 maximize_path_class: props.maximize_path_class,
1347 maximize_button_type: props.maximize_button_type,
1348 maximize_aria_label: props.maximize_aria_label,
1349 maximize_title: props.maximize_title,
1350 maximize_tabindex: props.maximize_tabindex,
1351 share_button_style: props.share_button_style,
1352 share_onclick: props.share_onclick,
1353 share_onmouseover: props.share_onmouseover,
1354 share_onmouseout: props.share_onmouseout,
1355 share_onfocus: props.share_onfocus,
1356 share_onblur: props.share_onblur,
1357 share_tabindex: props.share_tabindex,
1358 tabs_button_style: props.tabs_button_style,
1359 tabs_onclick: props.tabs_onclick,
1360 tabs_onmouseover: props.tabs_onmouseover,
1361 tabs_onmouseout: props.tabs_onmouseout,
1362 tabs_onfocus: props.tabs_onfocus,
1363 tabs_onblur: props.tabs_onblur,
1364 tabs_tabindex: props.tabs_tabindex,
1365 more_button_style: props.more_button_style,
1366 more_onclick: props.more_onclick,
1367 more_onmouseover: props.more_onmouseover,
1368 more_onmouseout: props.more_onmouseout,
1369 more_onfocus: props.more_onfocus,
1370 more_onblur: props.more_onblur,
1371 more_tabindex: props.more_tabindex,
1372 }
1373 BrowserContent {
1374 aria_describedby: props.aria_describedby,
1375 {props.children}
1376 }
1377 }
1378 }
1379}