1use leptix_core::compose_refs::use_composed_refs;
2use leptix_core::dismissable_layer::use_dismissable_layer;
3use leptix_core::focus_scope::use_focus_scope;
4use leptix_core::id::use_id;
5use leptix_core::portal::Portal;
6use leptix_core::presence::use_presence;
7use leptix_core::primitive::Primitive;
8use leptos::{context::Provider, ev::KeyboardEvent, html, prelude::*};
9use leptos_node_ref::AnyNodeRef;
10use send_wrapper::SendWrapper;
11use web_sys::wasm_bindgen::JsCast;
12
13#[derive(Clone, Debug)]
14struct ContextMenuContextValue {
15 open: RwSignal<bool>,
16 content_id: String,
17 position_x: RwSignal<f64>,
18 position_y: RwSignal<f64>,
19}
20
21#[component]
22pub fn ContextMenu(
23 #[prop(into, optional)] on_open_change: Option<Callback<bool>>,
24 children: TypedChildrenFn<impl IntoView + 'static>,
25) -> impl IntoView {
26 let children = StoredValue::new(children.into_inner());
27 let open = RwSignal::new(false);
28 let base_id = use_id(None).get();
29
30 Effect::new(move |_| {
31 if let Some(cb) = on_open_change {
32 cb.run(open.get());
33 }
34 });
35
36 let ctx = ContextMenuContextValue {
37 open,
38 content_id: format!("{}-ctx", base_id),
39 position_x: RwSignal::new(0.0),
40 position_y: RwSignal::new(0.0),
41 };
42
43 view! {
44 <Provider value=ctx>
45 {children.with_value(|c| c())}
46 </Provider>
47 }
48}
49
50#[component]
51pub fn ContextMenuTrigger(
52 #[prop(into, optional)] disabled: MaybeProp<bool>,
53 #[prop(into, optional)] as_child: MaybeProp<bool>,
54 #[prop(into, optional)] node_ref: AnyNodeRef,
55 children: TypedChildrenFn<impl IntoView + 'static>,
56) -> impl IntoView {
57 let children = StoredValue::new(children.into_inner());
58 let ctx = expect_context::<ContextMenuContextValue>();
59 let disabled = Signal::derive(move || disabled.get().unwrap_or(false));
60
61 view! {
62 <Primitive
63 element=html::span
64 as_child=as_child
65 node_ref=node_ref
66 attr:data-state=move || if ctx.open.get() { "open" } else { "closed" }
67 attr:data-disabled=move || disabled.get().then_some("")
68 on:contextmenu=move |event: web_sys::MouseEvent| {
69 if !disabled.get() {
70 event.prevent_default();
71 ctx.position_x.set(event.page_x() as f64);
74 ctx.position_y.set(event.page_y() as f64);
75 ctx.open.set(true);
76 }
77 }
78 >
79 {children.with_value(|c| c())}
80 </Primitive>
81 }
82}
83
84#[component]
85pub fn ContextMenuPortal(
86 #[prop(into, optional)] container: MaybeProp<SendWrapper<web_sys::Element>>,
87 #[prop(into, optional)] container_ref: AnyNodeRef,
88 #[prop(into, optional)] _force_mount: MaybeProp<bool>,
89 children: TypedChildrenFn<impl IntoView + 'static>,
90) -> impl IntoView {
91 let children = StoredValue::new(children.into_inner());
92 let ctx = expect_context::<ContextMenuContextValue>();
93 let ctx_for_portal = StoredValue::new(ctx.clone());
94 view! {
95 <Show when=move || ctx.open.get()>
96 <Portal container=container container_ref=container_ref>
97 <Provider value=ctx_for_portal.get_value()>
98 {children.with_value(|c| c())}
99 </Provider>
100 </Portal>
101 </Show>
102 }
103}
104
105#[component]
106pub fn ContextMenuContent(
107 #[prop(into, optional)] as_child: MaybeProp<bool>,
108 #[prop(into, optional)] node_ref: AnyNodeRef,
109 children: TypedChildrenFn<impl IntoView + 'static>,
110) -> impl IntoView {
111 let children = StoredValue::new(children.into_inner());
112 let ctx = expect_context::<ContextMenuContextValue>();
113 let open = Signal::derive(move || ctx.open.get());
114 let presence = use_presence(open);
115
116 let focus_ref = use_focus_scope(Signal::derive(|| true), Signal::derive(|| true), None, None);
117 let dismiss_ref = use_dismissable_layer(
118 None,
119 None,
120 None,
121 None,
122 Some(Callback::new(move |()| ctx.open.set(false))),
123 Signal::derive(move || !ctx.open.get()),
124 );
125 let refs = use_composed_refs(vec![node_ref, presence.node_ref, focus_ref, dismiss_ref]);
126 let search_buffer: RwSignal<String> = RwSignal::new(String::new());
127 let search_timer: RwSignal<Option<i32>> = RwSignal::new(None);
128
129 view! {
130 <Show when=move || presence.is_present.get()>
131 <Primitive
132 element=html::div
133 as_child=as_child
134 node_ref=refs
135 attr:id=ctx.content_id.clone()
136 attr:role="menu"
137 attr:data-state=move || if ctx.open.get() { "open" } else { "closed" }
138 attr:tabindex="-1"
139 attr:style=move || format!("position:absolute;left:{}px;top:{}px;z-index:50;", ctx.position_x.get(), ctx.position_y.get())
140 on:keydown=move |event: KeyboardEvent| {
141 match event.key().as_str() {
142 "Tab" => { event.prevent_default(); }
143 "ArrowDown" | "PageDown" => { event.prevent_default(); focus_menu_item(&event, true); }
144 "ArrowUp" | "PageUp" => { event.prevent_default(); focus_menu_item(&event, false); }
145 "Home" => { event.prevent_default(); focus_menu_item_edge(&event, true); }
146 "End" => { event.prevent_default(); focus_menu_item_edge(&event, false); }
147 key if key.len() == 1 && !event.ctrl_key() && !event.meta_key() => {
148 handle_typeahead(&event, key, search_buffer, search_timer);
149 }
150 _ => {}
151 }
152 }
153 >
154 {children.with_value(|c| c())}
155 </Primitive>
156 </Show>
157 }
158}
159
160#[component]
164pub fn ContextMenuItem(
165 #[prop(into, optional)] disabled: MaybeProp<bool>,
166 #[prop(into, optional)] on_select: Option<Callback<()>>,
167 #[prop(into, optional)] as_child: MaybeProp<bool>,
168 #[prop(into, optional)] node_ref: AnyNodeRef,
169 children: TypedChildrenFn<impl IntoView + 'static>,
170) -> impl IntoView {
171 let children = StoredValue::new(children.into_inner());
172 let ctx = expect_context::<ContextMenuContextValue>();
173 let disabled = Signal::derive(move || disabled.get().unwrap_or(false));
174
175 view! {
176 <Primitive element=html::div as_child=as_child node_ref=node_ref
177 attr:role="menuitem"
178 attr:data-disabled=move || disabled.get().then_some("")
179 attr:tabindex="-1"
180 on:click=move |_| {
181 if !disabled.get() {
182 if let Some(cb) = on_select { cb.run(()); }
183 ctx.open.set(false);
184 }
185 }
186 on:keydown=move |event: KeyboardEvent| {
187 if matches!(event.key().as_str(), "Enter" | " ") && !disabled.get() {
188 event.prevent_default();
189 if let Some(cb) = on_select { cb.run(()); }
190 ctx.open.set(false);
191 }
192 }
193 >
194 {children.with_value(|c| c())}
195 </Primitive>
196 }
197}
198
199#[component]
200pub fn ContextMenuSeparator(
201 #[prop(into, optional)] as_child: MaybeProp<bool>,
202 #[prop(into, optional)] node_ref: AnyNodeRef,
203) -> impl IntoView {
204 view! {
205 <Primitive element=html::div as_child=as_child node_ref=node_ref
206 attr:role="separator"
207 attr:aria-orientation="horizontal"
208 >
209 {""}
210 </Primitive>
211 }
212}
213
214#[component]
215pub fn ContextMenuLabel(
216 #[prop(into, optional)] as_child: MaybeProp<bool>,
217 #[prop(into, optional)] node_ref: AnyNodeRef,
218 children: TypedChildrenFn<impl IntoView + 'static>,
219) -> impl IntoView {
220 let children = StoredValue::new(children.into_inner());
221 view! {
222 <Primitive element=html::div as_child=as_child node_ref=node_ref>
223 {children.with_value(|c| c())}
224 </Primitive>
225 }
226}
227
228#[component]
229pub fn ContextMenuGroup(
230 #[prop(into, optional)] as_child: MaybeProp<bool>,
231 #[prop(into, optional)] node_ref: AnyNodeRef,
232 children: TypedChildrenFn<impl IntoView + 'static>,
233) -> impl IntoView {
234 let children = StoredValue::new(children.into_inner());
235 view! {
236 <Primitive element=html::div as_child=as_child node_ref=node_ref attr:role="group">
237 {children.with_value(|c| c())}
238 </Primitive>
239 }
240}
241
242#[derive(Clone, Debug)]
247struct ContextMenuItemCheckedContextValue {
248 checked: Signal<bool>,
249}
250
251#[component]
252pub fn ContextMenuCheckboxItem(
253 #[prop(into, optional)] checked: MaybeProp<bool>,
254 #[prop(into, optional)] on_checked_change: Option<Callback<bool>>,
255 #[prop(into, optional)] disabled: MaybeProp<bool>,
256 #[prop(into, optional)] on_select: Option<Callback<()>>,
257 #[prop(into, optional)] as_child: MaybeProp<bool>,
258 #[prop(into, optional)] node_ref: AnyNodeRef,
259 children: TypedChildrenFn<impl IntoView + 'static>,
260) -> impl IntoView {
261 let children = StoredValue::new(children.into_inner());
262 let ctx = expect_context::<ContextMenuContextValue>();
263 let disabled = Signal::derive(move || disabled.get().unwrap_or(false));
264 let checked = Signal::derive(move || checked.get().unwrap_or(false));
265
266 let item_checked_ctx = ContextMenuItemCheckedContextValue { checked };
267
268 view! {
269 <Provider value=item_checked_ctx>
270 <Primitive element=html::div as_child=as_child node_ref=node_ref
271 attr:role="menuitemcheckbox"
272 attr:aria-checked=move || checked.get().to_string()
273 attr:data-state=move || if checked.get() { "checked" } else { "unchecked" }
274 attr:data-disabled=move || disabled.get().then_some("")
275 attr:tabindex="-1"
276 on:click=move |_| {
277 if !disabled.get() {
278 if let Some(cb) = on_checked_change { cb.run(!checked.get()); }
279 if let Some(cb) = on_select { cb.run(()); }
280 ctx.open.set(false);
281 }
282 }
283 on:keydown=move |event: KeyboardEvent| {
284 if matches!(event.key().as_str(), "Enter" | " ") && !disabled.get() {
285 event.prevent_default();
286 if let Some(cb) = on_checked_change { cb.run(!checked.get()); }
287 if let Some(cb) = on_select { cb.run(()); }
288 ctx.open.set(false);
289 }
290 }
291 >
292 {children.with_value(|c| c())}
293 </Primitive>
294 </Provider>
295 }
296}
297
298#[derive(Clone, Debug)]
303struct ContextMenuRadioGroupContextValue {
304 value: Signal<Option<String>>,
305 on_value_change: Callback<String>,
306}
307
308#[component]
309pub fn ContextMenuRadioGroup(
310 #[prop(into, optional)] value: MaybeProp<String>,
311 #[prop(into, optional)] on_value_change: Option<Callback<String>>,
312 #[prop(into, optional)] as_child: MaybeProp<bool>,
313 #[prop(into, optional)] node_ref: AnyNodeRef,
314 children: TypedChildrenFn<impl IntoView + 'static>,
315) -> impl IntoView {
316 let children = StoredValue::new(children.into_inner());
317 let value = Signal::derive(move || value.get());
318 let radio_ctx = ContextMenuRadioGroupContextValue {
319 value,
320 on_value_change: Callback::new(move |v: String| {
321 if let Some(cb) = on_value_change {
322 cb.run(v);
323 }
324 }),
325 };
326
327 view! {
328 <Provider value=radio_ctx>
329 <Primitive element=html::div as_child=as_child node_ref=node_ref attr:role="group">
330 {children.with_value(|c| c())}
331 </Primitive>
332 </Provider>
333 }
334}
335
336#[component]
337pub fn ContextMenuRadioItem(
338 #[prop(into)] value: String,
339 #[prop(into, optional)] disabled: MaybeProp<bool>,
340 #[prop(into, optional)] on_select: Option<Callback<()>>,
341 #[prop(into, optional)] as_child: MaybeProp<bool>,
342 #[prop(into, optional)] node_ref: AnyNodeRef,
343 children: TypedChildrenFn<impl IntoView + 'static>,
344) -> impl IntoView {
345 let children = StoredValue::new(children.into_inner());
346 let ctx = expect_context::<ContextMenuContextValue>();
347 let radio_ctx = expect_context::<ContextMenuRadioGroupContextValue>();
348 let item_value = value.clone();
349 let item_value_click = value.clone();
350 let disabled = Signal::derive(move || disabled.get().unwrap_or(false));
351 let checked =
352 Signal::derive(move || radio_ctx.value.get().as_deref() == Some(item_value.as_str()));
353 let item_checked_ctx = ContextMenuItemCheckedContextValue { checked };
354
355 view! {
356 <Provider value=item_checked_ctx>
357 <Primitive element=html::div as_child=as_child node_ref=node_ref
358 attr:role="menuitemradio"
359 attr:aria-checked=move || checked.get().to_string()
360 attr:data-state=move || if checked.get() { "checked" } else { "unchecked" }
361 attr:data-disabled=move || disabled.get().then_some("")
362 attr:tabindex="-1"
363 on:click=move |_| {
364 if !disabled.get() {
365 radio_ctx.on_value_change.run(item_value_click.clone());
366 if let Some(cb) = on_select { cb.run(()); }
367 ctx.open.set(false);
368 }
369 }
370 on:keydown=move |event: KeyboardEvent| {
371 if matches!(event.key().as_str(), "Enter" | " ") && !disabled.get() {
372 event.prevent_default();
373 radio_ctx.on_value_change.run(value.clone());
374 if let Some(cb) = on_select { cb.run(()); }
375 ctx.open.set(false);
376 }
377 }
378 >
379 {children.with_value(|c| c())}
380 </Primitive>
381 </Provider>
382 }
383}
384
385#[component]
390pub fn ContextMenuItemIndicator(
391 #[prop(into, optional)] force_mount: MaybeProp<bool>,
392 #[prop(into, optional)] as_child: MaybeProp<bool>,
393 #[prop(into, optional)] node_ref: AnyNodeRef,
394 #[prop(optional)] children: Option<ChildrenFn>,
395) -> impl IntoView {
396 let children = StoredValue::new(children);
397 let force_mount = Signal::derive(move || force_mount.get().unwrap_or(false));
398 let checked_ctx = expect_context::<ContextMenuItemCheckedContextValue>();
399
400 view! {
401 <Show when=move || force_mount.get() || checked_ctx.checked.get()>
402 <Primitive element=html::span as_child=as_child node_ref=node_ref
403 attr:data-state=move || if checked_ctx.checked.get() { "checked" } else { "unchecked" }
404 >
405 {children.with_value(|c| c.as_ref().map(|c| c()))}
406 </Primitive>
407 </Show>
408 }
409}
410
411#[derive(Clone, Debug)]
416struct ContextMenuSubContextValue {
417 open: RwSignal<bool>,
418 content_id: String,
419 trigger_ref: AnyNodeRef,
420}
421
422#[component]
423pub fn ContextMenuSub(
424 #[prop(into, optional)] open: MaybeProp<bool>,
425 #[prop(into, optional)] default_open: MaybeProp<bool>,
426 #[prop(into, optional)] on_open_change: Option<Callback<bool>>,
427 children: TypedChildrenFn<impl IntoView + 'static>,
428) -> impl IntoView {
429 let children = StoredValue::new(children.into_inner());
430 let open_state = RwSignal::new(open.get().or(default_open.get()).unwrap_or(false));
431
432 Effect::new(move |_| {
433 if let Some(o) = open.get() {
434 open_state.set(o);
435 }
436 });
437 Effect::new(move |_| {
438 if let Some(cb) = on_open_change {
439 cb.run(open_state.get());
440 }
441 });
442
443 let base_id = use_id(None).get();
444 let sub_ctx = ContextMenuSubContextValue {
445 open: open_state,
446 content_id: format!("{}-sub", base_id),
447 trigger_ref: AnyNodeRef::new(),
448 };
449
450 view! {
451 <Provider value=sub_ctx>
452 {children.with_value(|c| c())}
453 </Provider>
454 }
455}
456
457#[component]
458pub fn ContextMenuSubTrigger(
459 #[prop(into, optional)] disabled: MaybeProp<bool>,
460 #[prop(into, optional)] as_child: MaybeProp<bool>,
461 #[prop(into, optional)] node_ref: AnyNodeRef,
462 children: TypedChildrenFn<impl IntoView + 'static>,
463) -> impl IntoView {
464 let children = StoredValue::new(children.into_inner());
465 let sub_ctx = expect_context::<ContextMenuSubContextValue>();
466 let disabled = Signal::derive(move || disabled.get().unwrap_or(false));
467 let refs = use_composed_refs(vec![node_ref, sub_ctx.trigger_ref]);
468
469 view! {
470 <Primitive element=html::div as_child=as_child node_ref=refs
471 attr:role="menuitem"
472 attr:aria-haspopup="menu"
473 attr:aria-expanded=move || sub_ctx.open.get().to_string()
474 attr:aria-controls=move || sub_ctx.open.get().then(|| sub_ctx.content_id.clone())
475 attr:data-state=move || if sub_ctx.open.get() { "open" } else { "closed" }
476 attr:data-disabled=move || disabled.get().then_some("")
477 attr:tabindex="-1"
478 on:click=move |_| {
479 if !disabled.get() { sub_ctx.open.set(!sub_ctx.open.get()); }
480 }
481 on:pointerenter=move |_| {
482 if !disabled.get() { sub_ctx.open.set(true); }
483 }
484 on:keydown=move |event: KeyboardEvent| {
485 if event.key() == "ArrowRight" && !disabled.get() {
486 event.prevent_default();
487 sub_ctx.open.set(true);
488 }
489 }
490 >
491 {children.with_value(|c| c())}
492 </Primitive>
493 }
494}
495
496#[component]
497pub fn ContextMenuSubContent(
498 #[prop(into, optional)] force_mount: MaybeProp<bool>,
499 #[prop(into, optional)] as_child: MaybeProp<bool>,
500 #[prop(into, optional)] node_ref: AnyNodeRef,
501 children: TypedChildrenFn<impl IntoView + 'static>,
502) -> impl IntoView {
503 let children = StoredValue::new(children.into_inner());
504 let sub_ctx = expect_context::<ContextMenuSubContextValue>();
505 let force_mount = Signal::derive(move || force_mount.get().unwrap_or(false));
506 let present = Signal::derive(move || force_mount.get() || sub_ctx.open.get());
507 let presence = use_presence(present);
508
509 let dismiss_ref = use_dismissable_layer(
510 None,
511 None,
512 None,
513 None,
514 Some(Callback::new(move |()| sub_ctx.open.set(false))),
515 Signal::derive(move || !sub_ctx.open.get()),
516 );
517 let refs = use_composed_refs(vec![node_ref, presence.node_ref, dismiss_ref]);
518
519 view! {
520 <Show when=move || presence.is_present.get()>
521 <Primitive element=html::div as_child=as_child node_ref=refs
522 attr:id=sub_ctx.content_id.clone()
523 attr:role="menu"
524 attr:aria-orientation="vertical"
525 attr:data-state=move || if sub_ctx.open.get() { "open" } else { "closed" }
526 attr:tabindex="-1"
527 on:keydown=move |event: KeyboardEvent| {
528 match event.key().as_str() {
529 "ArrowDown" => { event.prevent_default(); focus_menu_item(&event, true); }
530 "ArrowUp" => { event.prevent_default(); focus_menu_item(&event, false); }
531 "ArrowLeft" => { event.prevent_default(); sub_ctx.open.set(false); }
532 "Escape" => { sub_ctx.open.set(false); }
533 _ => {}
534 }
535 }
536 on:pointerleave=move |_| { sub_ctx.open.set(false); }
537 >
538 {children.with_value(|c| c())}
539 </Primitive>
540 </Show>
541 }
542}
543
544#[component]
549pub fn ContextMenuArrow(
550 #[prop(into, optional)] width: MaybeProp<f64>,
551 #[prop(into, optional)] height: MaybeProp<f64>,
552 #[prop(into, optional)] as_child: MaybeProp<bool>,
553 #[prop(into, optional)] node_ref: AnyNodeRef,
554 #[prop(optional)] children: Option<ChildrenFn>,
555) -> impl IntoView {
556 let children = StoredValue::new(children);
557 let _width = width;
560 let _height = height;
561 view! {
562 <Primitive element=html::span as_child=as_child node_ref=node_ref>
563 {children.with_value(|c| c.as_ref().map(|c| c()))}
564 </Primitive>
565 }
566}
567
568fn focus_menu_item(event: &KeyboardEvent, forward: bool) {
573 let Some(container) = event.current_target().and_then(|t| {
574 use web_sys::wasm_bindgen::JsCast;
575 t.dyn_into::<web_sys::Element>().ok()
576 }) else {
577 return;
578 };
579 let Ok(items) = container.query_selector_all(
580 "[role='menuitem']:not([data-disabled]), [role='menuitemcheckbox']:not([data-disabled]), [role='menuitemradio']:not([data-disabled])"
581 ) else {
582 return;
583 };
584 let mut nodes = vec![];
585 for i in 0..items.length() {
586 if let Some(n) = items.item(i) {
587 nodes.push(n);
588 }
589 }
590 let active = web_sys::window()
591 .and_then(|w| w.document())
592 .and_then(|d| d.active_element());
593 let idx = active.as_ref().and_then(|a| {
594 nodes
595 .iter()
596 .position(|n| n == <web_sys::Element as AsRef<web_sys::Node>>::as_ref(a))
597 });
598 let next = if forward {
599 idx.map(|i| i + 1).filter(|i| *i < nodes.len()).or(Some(0))
600 } else {
601 idx.and_then(|i| i.checked_sub(1))
602 .or(Some(nodes.len().saturating_sub(1)))
603 };
604 if let Some(i) = next
605 && let Some(n) = nodes.get(i)
606 {
607 use web_sys::wasm_bindgen::JsCast;
608 if let Ok(el) = n.clone().dyn_into::<web_sys::HtmlElement>() {
609 let _ = el.focus();
610 }
611 }
612}
613
614fn focus_menu_item_edge(event: &KeyboardEvent, first: bool) {
615 let Some(container) = event.current_target().and_then(|t| {
616 use web_sys::wasm_bindgen::JsCast;
617 t.dyn_into::<web_sys::Element>().ok()
618 }) else {
619 return;
620 };
621 let Ok(items) = container.query_selector_all(MENU_ITEM_SELECTOR) else {
622 return;
623 };
624 let target_idx = if first {
625 0
626 } else {
627 items.length().saturating_sub(1)
628 };
629 if let Some(node) = items.item(target_idx) {
630 use web_sys::wasm_bindgen::JsCast;
631 if let Ok(el) = node.dyn_into::<web_sys::HtmlElement>() {
632 let _ = el.focus();
633 }
634 }
635}
636
637fn handle_typeahead(
638 event: &KeyboardEvent,
639 key: &str,
640 search_buffer: RwSignal<String>,
641 search_timer: RwSignal<Option<i32>>,
642) {
643 let Some(container) = event.current_target().and_then(|t| {
644 use web_sys::wasm_bindgen::JsCast;
645 t.dyn_into::<web_sys::Element>().ok()
646 }) else {
647 return;
648 };
649 if let Some(id) = search_timer.get_untracked()
650 && let Some(w) = web_sys::window()
651 {
652 w.clear_timeout_with_handle(id);
653 }
654 search_buffer.update(|buf| buf.push_str(key));
655 let id = web_sys::window().and_then(|w| {
656 w.set_timeout_with_callback_and_timeout_and_arguments_0(
657 web_sys::wasm_bindgen::closure::Closure::<dyn Fn()>::new(move || {
658 search_buffer.set(String::new());
659 })
660 .into_js_value()
661 .unchecked_ref(),
662 1000,
663 )
664 .ok()
665 });
666 search_timer.set(id);
667
668 let search = search_buffer.get_untracked().to_lowercase();
669 let Ok(items) = container.query_selector_all(MENU_ITEM_SELECTOR) else {
670 return;
671 };
672 for i in 0..items.length() {
673 if let Some(node) = items.item(i) {
674 use web_sys::wasm_bindgen::JsCast;
675 if let Some(text) = node.text_content()
676 && text.trim().to_lowercase().starts_with(&search)
677 && let Ok(el) = node.dyn_into::<web_sys::HtmlElement>()
678 {
679 let _ = el.focus();
680 return;
681 }
682 }
683 }
684}
685
686const MENU_ITEM_SELECTOR: &str = "[role='menuitem']:not([data-disabled]), [role='menuitemcheckbox']:not([data-disabled]), [role='menuitemradio']:not([data-disabled])";