yew_bootstrap/component/tooltip.rs
1//! Implements tooltip suppport.
2//!
3//! `yew` presumes it has exclusive control of the DOM, which conflicts with the
4//! Bootstrap's assumption that it also has exclusive control of the DOM.
5//!
6//! So, we need to re-implement the Tooltip plugin using `yew`...
7//!
8//! * <https://github.com/react-bootstrap/react-bootstrap/blob/master/src/Tooltip.tsx>
9//! * <https://github.com/twbs/bootstrap/blob/main/js/src/tooltip.js>
10
11pub use popper_rs::prelude::Placement;
12use popper_rs::{
13 prelude::{use_popper, Modifier, Offset, Options, Strategy},
14 state::ApplyAttributes,
15};
16use wasm_bindgen::{closure::Closure, JsCast};
17use web_sys::{HtmlElement, MediaQueryList, MediaQueryListEvent};
18use yew::{html::IntoPropValue, platform::spawn_local, prelude::*};
19
20/// Media query to indicate that the primary pointing device is missing or does
21/// not support hovering.
22///
23/// Reference: [Media Queries Level 4: Hover Capability](https://www.w3.org/TR/mediaqueries-4/#hover)
24const MEDIA_QUERY_HOVER_NONE: &str = "(hover: none)";
25
26/// Media query to indicate that there is no pointing device which supports
27/// hovering.
28///
29/// Reference: [Media Queries Level 4: All Available Interaction Capabilities](https://www.w3.org/TR/mediaqueries-4/#any-input)
30const MEDIA_QUERY_ANY_HOVER_NONE: &str = "(any-hover: none)";
31
32/// Media query to indicate that there are either no pointing devices, or a
33/// pointing device only supports coarse input.
34///
35/// Reference: [Media Queries Level 4: All Available Interaction Capabilities](https://www.w3.org/TR/mediaqueries-4/#any-input)
36const MEDIA_QUERY_ANY_POINTER_NONE_OR_COARSE: &str = "(any-pointer: none) or (any-pointer: coarse)";
37
38/// Trigger options for [`TooltipProps::trigger_on_focus`].
39///
40/// This allows tooltips to be selectively enabled on focus, depending on the
41/// result of which [Interaction Media Features][0] the user's device supports.
42///
43/// [0]: https://www.w3.org/TR/mediaqueries-4/#mf-interaction
44#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
45pub enum TooltipFocusTrigger {
46 /// Always show the tooltip on element focus.
47 ///
48 /// This is the default option, and provides a reliable and accessible
49 /// alternative when using a non-hover-capable device (such as a
50 /// touchscreen) or navigating with a keyboard on a device that *also* has a
51 /// pointing device.
52 ///
53 /// Because of the many side-effects, browser and platform bugs that come
54 /// from attempting to *selectively* disable showing tooltips on focus, this
55 /// is generally the best choice, but may lead to unexpected tooltip display
56 /// for users on a desktop browser with a traditional mouse.
57 #[default]
58 Always,
59
60 /// Show the tooltip on element focus only if the *primary* pointing device
61 /// does *not* support hovering (eg: touchscreen), or there are no pointing
62 /// devices connected (`hover: none`).
63 ///
64 /// If the primary pointing device supports hovering (eg: mouse, trackpad,
65 /// trackball, smart pen, Wiimote, Leap Motion Controller), the tooltip will
66 /// not be shown when the element has focus.
67 ///
68 /// Figuring out what the "primary" pointing device actually is
69 /// [can be complicated to answer for some devices][0]. They generally err
70 /// towards reporting the use and presence of an ordinary mouse with hover
71 /// capabilities (eg: [Firefox bug 1851244][1]), even when there are no
72 /// pointing devices connected, or used with a touchscreen.
73 ///
74 /// Both [Chromium][2] and [Firefox][3] on Windows erroneously report
75 /// touch-only devices as having an ordinary mouse with hover capabilities
76 /// if the device lacks an auto-rotation sensor (even if disabled), which is
77 /// generally the case for non-tablet devices like external touchscreen
78 /// monitors and all-in-one PCs).
79 ///
80 /// [Some Android devices also erroneously report `hover: hover`][4], even
81 /// when they only have a touch screen.
82 ///
83 /// Safari on iOS *always* reports `hover: none`, even when using an iPad
84 /// with hover-capable pointing devices, such as the Apple Pencil (stylus),
85 /// Magic Keyboard (trackpad) or an ordinary mouse with an external display.
86 /// All hover-capable devices *except* the Pencil are reported via
87 /// `any-hover: hover`.
88 ///
89 /// For someone who primarily uses a keyboard to interact with their
90 /// computer, but has a mouse plugged in (eg: a laptop with a built-in
91 /// trackpad, or a virtual device), their browser will still report a
92 /// primary pointing device which is "hover capable", even when they have no
93 /// way to hover.
94 ///
95 /// These implementation problems and shortfalls make the `hover: none`
96 /// media query unreliable, and an unreliable indicator of user preferences.
97 ///
98 /// [0]: https://firefox-source-docs.mozilla.org/widget/windows/windows-pointing-device/index.html#selection-of-the-primary-pointing-device
99 /// [1]: https://bugzilla.mozilla.org/show_bug.cgi?id=1851244
100 /// [2]: https://issues.chromium.org/issues/366055333
101 /// [3]: https://bugzilla.mozilla.org/show_bug.cgi?id=1918292
102 /// [4]: https://issues.chromium.org/issues/41445959
103 IfHoverNone,
104
105 /// Trigger showing the tooltip on element focus only if *all* pointing
106 /// devices connected to the device do not support hovering, or there there
107 /// are no pointing devices connected (`any-hover: none`).
108 ///
109 /// For a device with *only* one non-hovering pointing device (eg: a mobile
110 /// phone with a touch screen or basic stylus), this is the same as
111 /// [`TooltipFocusTrigger::IfHoverNone`].
112 ///
113 /// For a device with *both* hovering and non-hovering pointing device(s)
114 /// (eg: a laptop with a trackpad and touchscreen, or a tablet with both pen
115 /// and touch input), this option will never show will never show the
116 /// tooltip on focus.
117 ///
118 /// Unfortunately, [there is no way to detect if not *all* pointer devices support hovering][1].
119 ///
120 /// Most desktop browsers will *always* report the presence of an ordinary
121 /// (hover-capable) mouse, even if none is attached. This can be caused by:
122 ///
123 /// * a wireless mouse dongle which is plugged in, but the wireless mouse
124 /// itself is turned off
125 ///
126 /// * the presence of a PS/2 mouse controller
127 ///
128 /// * the presence of a virtual mouse device
129 ///
130 /// * a touch screen which does not have an automatic rotation sensor
131 /// (but this will report hover events from touch), due to [Chromium][2]
132 /// and [Firefox][3] bugs.
133 ///
134 /// These issues may also impact someone who primarily uses a keyboard to
135 /// interact with their computer.
136 ///
137 /// These implementation problems and shortfalls make the `any-hover: none`
138 /// media query an unreliable indicator of user preferences.
139 ///
140 /// [1]: https://github.com/w3c/csswg-drafts/issues/5462
141 /// [2]: https://issues.chromium.org/issues/366055333
142 /// [3]: https://bugzilla.mozilla.org/show_bug.cgi?id=1918292
143 IfAnyHoverNone,
144
145 /// Trigger showing tooltips on element focus only if:
146 ///
147 /// * there are no pointer devices present (`any-pointer: none`), **or**,
148 /// * there are *coarse* pointer devices present (`any-pointer: coarse`),
149 /// such as a touchscreen, Wiimote or Leap Motion Controller
150 ///
151 /// This is a work-around for there being
152 /// [no way for a browser to report that not all devices support `hover`][1],
153 /// and the complex heuristics required (which all browsers lack) to
154 /// determine which is the "primary" pointing device on desktop and laptop
155 /// computers.
156 ///
157 /// The intent of this mode is that tooltips will be shown on `focus` for
158 /// devices with touchscreens, *regardless* of whether they have an
159 /// auto-rotation sensor.
160 ///
161 /// The side-effects are:
162 ///
163 /// * hovering `coarse` pointer devices (like the Wiimote and Leap Motion
164 /// Controller) will *also* show tooltips on focus, even though they can
165 /// hover
166 /// * traditional-style laptops with touchscreens (ie: not foldable or
167 /// convertible into a tablet) will *also* show tooltips on focus, even
168 /// though using the touchscreen as a primary pointing device is very
169 /// uncomfortable (because it requires reaching over the keyboard)
170 /// * non-hovering `fine` pointer devices (like basic stylus digitisers)
171 /// will *not* show tooltips on focus, even though they can't hover
172 /// * a user primarily using non-pointer (keyboard) input but with at least
173 /// one `fine` pointing device connected (such as a laptop with built-in
174 /// trackpad) will never see tooltips on focus
175 /// * [Safari doesn't fire `focus` events][2] on components on click or
176 /// touch if it does not accept keyboard input (eg: `<a>` and `<button>`)
177 ///
178 /// [1]: https://github.com/w3c/csswg-drafts/issues/5462
179 /// [2]: https://webkit.org/b/22261#c68
180 IfAnyPointerNoneOrCoarse,
181
182 /// Never show the tooltip on element focus.
183 ///
184 /// Make sure there is some other way to trigger the tooltip which works on
185 /// all types of devices and meets users' preferred input modalities.
186 Never,
187}
188
189impl IntoPropValue<TooltipFocusTrigger> for bool {
190 fn into_prop_value(self) -> TooltipFocusTrigger {
191 if self {
192 TooltipFocusTrigger::Always
193 } else {
194 TooltipFocusTrigger::Never
195 }
196 }
197}
198
199impl TooltipFocusTrigger {
200 fn media_queries(&self) -> Option<MediaQueryList> {
201 let query = match self {
202 Self::Always | Self::Never => return None,
203 Self::IfHoverNone => MEDIA_QUERY_HOVER_NONE,
204 Self::IfAnyHoverNone => MEDIA_QUERY_ANY_HOVER_NONE,
205 Self::IfAnyPointerNoneOrCoarse => MEDIA_QUERY_ANY_POINTER_NONE_OR_COARSE,
206 };
207 let w = gloo_utils::window();
208 w.match_media(query).ok().flatten()
209 }
210
211 fn should_trigger(&self) -> bool {
212 let Some(queries) = self.media_queries() else {
213 return match self {
214 Self::Always => true,
215 Self::Never => false,
216 _ => unreachable!(),
217 };
218 };
219
220 queries.matches()
221 }
222}
223
224#[derive(Properties, Clone, PartialEq)]
225pub struct TooltipProps {
226 /// The node which this tooltip is attached to.
227 ///
228 /// If the `target` can be `disabled`, pass the same value to
229 /// [Tooltip's `disabled` property][Self::disabled] to ensure that the
230 /// tooltip will be automatically hidden, even if it had focus, was being
231 /// hovered or clicked.
232 pub target: NodeRef,
233
234 /// ID of the tooltip.
235 ///
236 /// If this is set, [Tooltip] will set the `target`'s `aria-describedby`
237 /// attribute whenever it is visible.
238 #[prop_or_default]
239 pub id: Option<AttrValue>,
240
241 /// Content of the tooltip.
242 #[prop_or_default]
243 pub children: Children,
244
245 /// Placement of the tooltip.
246 ///
247 /// [Popper's website shows all placement options][0].
248 ///
249 /// [0]: https://popper.js.org/
250 #[prop_or_default]
251 pub placement: Placement,
252
253 /// Use fade transition when showing or hiding the tooltip.
254 #[prop_or_default]
255 pub fade: bool,
256
257 /// If `true`, always show the tooltip, regardless of focus state.
258 ///
259 /// [`disabled = true`][TooltipProps::disabled] overrides this option.
260 #[prop_or_default]
261 pub show: bool,
262
263 /// Show the tooltip when the [`target`][Self::target] node recieves input
264 /// or keyboard focus.
265 ///
266 /// This defaults to [`TooltipFocusTrigger::Always`], which always shows the
267 /// tooltip on input focus. See [`TooltipFocusTrigger`] for other options
268 /// which selectively disable this behaviour based on media queries,
269 /// and full caveats on each.
270 ///
271 /// This [will not trigger on `disabled` elements][0].
272 ///
273 /// ## Safari/WebKit focus events
274 ///
275 /// *Unlike* most other web browsers (even on macOS), Safari and other
276 /// browsers using the WebKit renderer[^1] do not fire `focus` events for
277 /// components which *do not* accept keyboard input (such as `<a>` and
278 /// `<button>` elements) when clicked or touched,
279 /// [following macOS conventions][2].
280 ///
281 /// In Safari on macOS, pressing <kbd>Option</kbd> + <kbd>Tab</kbd> will
282 /// allow you to focus components that does not accept keyboard input, and
283 /// will fire a `focus` event similarly to other platforms. If
284 /// [keyboard navigation is enabled in System Settings][3], pressing
285 /// <kbd>Tab</kbd> cycles focus between any component (like other
286 /// platforms).
287 ///
288 /// Safari will fire `focus` **and** (potentially-synthetic) `hover` events
289 /// for components which accept keyboard input (such as
290 /// `<input type="text">`) when clicked *or touched*. Touchscreen devices on
291 /// most other platforms will only fire `focus` events on touch, not
292 /// `hover`.
293 ///
294 /// [^1]: [Outside of the European Union][4], **all** browsers on iOS *also* use WebKit.
295 ///
296 /// [0]: https://getbootstrap.com/docs/5.3/components/tooltips/#disabled-elements
297 /// [2]: https://webkit.org/b/22261#c68
298 /// [3]: https://support.apple.com/en-au/guide/mac-help/mchlp1399/14.0/mac/14.0
299 /// [4]: https://developer.apple.com/support/alternative-browser-engines/
300 #[prop_or_default]
301 pub trigger_on_focus: TooltipFocusTrigger,
302
303 /// Show the tooltip when the [`target`][Self::target] component has the
304 /// mouse cursor hovered over it.
305 ///
306 /// This defaults to `true`, but [will not trigger on `disabled` elements][0].
307 ///
308 /// **Note:** touchscreen devices and keyboard-only users *may not* trigger
309 /// hover events. Ensure there is some other way to trigger the tooltip on
310 /// those devices, such as with
311 /// [`trigger_on_focus={TooltipFocusTrigger::Always}`][1].
312 ///
313 /// Safari on iOS reports synthetic `mouseenter` events on touchscreen
314 /// devices, when browsers on other platforms with touchscreens
315 /// traditionally do not.
316 ///
317 /// However, [Safari on iOS does not fire `focus` events][2] for components
318 /// which do not accept keyboard input (such as `<a>` and `<button>`), so
319 /// synthetic `hover` events are the only way to trigger tooltips.
320 ///
321 /// [0]: https://getbootstrap.com/docs/5.3/components/tooltips/#disabled-elements
322 /// [1]: Self::trigger_on_focus
323 /// [2]: https://webkit.org/b/22261#c68
324 #[prop_or(true)]
325 pub trigger_on_hover: bool,
326
327 /// If `true`, always hide the tooltip. *This overrides all other
328 /// conditions.*
329 ///
330 /// The tooltip will remain part of the DOM.
331 ///
332 /// [Disabled elements don't fire events][0], including `focusout` on a
333 /// currently-focused element and `mouseleave` of a currently-hovered
334 /// element. This could cause a tooltip to be "stuck" being shown.
335 ///
336 /// This property allows you to automatically hide a [Tooltip] which has
337 /// [`trigger_on_focus = true`][Self::trigger_on_focus] or
338 /// [`trigger_on_hover = true`][Self::trigger_on_hover] whenever the
339 /// [`target`][Self::target] is disabled.
340 ///
341 /// **Warning:** entirely removing the [Tooltip] from the DOM whenever the
342 /// [`target`][Self::target] is disabled can cause strange behaviour if the
343 /// that tooltip is currently being displayed.
344 ///
345 /// [0]: https://getbootstrap.com/docs/5.3/components/tooltips/#disabled-elements
346 #[prop_or_default]
347 pub disabled: bool,
348}
349
350/// # Tooltip component
351///
352/// Tooltip which is automatically shown when an element is focused or hovered.
353///
354/// [Bootstrap's tooltips][0] depend on Popper, which assumes complete control
355/// of the DOM. Yew *also* assumes complete control of the DOM, so this can lead
356/// to unexpected behaviour whenever it reuses DOM components – so you can't
357/// just use `data-bs-toggle="tooltip"`.
358///
359/// This component is similar to [`react-bootstrap`'s Tooltip component][2] –
360/// it wires up Popper in a way that works nicely with Yew.
361///
362/// There are some similarities and differences between this component and
363/// Bootstrap's in-built implementation:
364///
365/// * There's no need to use `bootstrap.bundle.min.js` or `popper.min.js`. This
366/// component uses [`popper-rs`][], which comes with Popper.
367///
368/// * You can't trigger or describe tooltips with `data-bs-*` attributes.
369///
370/// * The `<Tooltip>`'s content is set with the [`children`][] property, which
371/// supports arbitrary HTML.
372///
373/// It doesn't support the `sanitize`, `sanitizeFn` or `title` attributes.
374///
375/// * Like Bootstrap, `<Tooltip>` is triggered by
376/// [input focus][trigger_on_focus] and [mouse hover][trigger_on_hover] by
377/// default.
378///
379/// These triggers can be individually disabled, and you can
380/// [control display manually][`show`] instead.
381///
382/// `<Tooltip>` *does not* support the `click` trigger – use input focus
383/// instead. This makes it possible to trigger tooltips when there is *no*
384/// pointing device available or it cannot be used.
385///
386/// * Like Bootstrap, tooltips exist in a shadow DOM ([portal][]) outside of the
387/// normal page hierarchy.
388///
389/// Unlike Bootstrap, the `<Tooltip>` is *always* present in the DOM, even
390/// when the tooltip is not displayed.
391///
392/// A `<Tooltip>` needs to remain part of the DOM if it *could* be shown in a
393/// component. Use the [`show`][] and [`disabled`][] properties to
394/// control its display.
395///
396/// * When using a [`target`][] which could be `disabled` and triggering
397/// on focus and/or on hover, you can prevent the tooltip from being displayed
398/// by setting the [`disabled`][] property on the on the `<Tooltip>`
399/// as well. Otherwise, the `target` won't fire an event to *hide* the tooltip
400/// when it loses focus or isn't hovered.
401///
402/// If you only ever trigger tooltips manually, then there's no need to sync
403/// the `disabled` state.
404///
405/// * Like Bootstrap, if you want the tooltip to be displayed on focus or on
406/// hover on a `disabled` [`target`][], you'll need to use a
407/// [wrapper element][].
408///
409/// ## Examples
410///
411/// Button with a tooltip, shown automatically on focus or hover:
412///
413/// ```rust
414/// use yew::prelude::*;
415/// use yew_bootstrap::component::{Button, Tooltip};
416/// use yew_bootstrap::util::Color;
417///
418/// fn test() -> Html {
419/// let btn_ref = NodeRef::default();
420/// html! {
421/// <>
422/// <Button style={Color::Primary} node_ref={btn_ref.clone()}>
423/// {"Button with tooltip"}
424/// </Button>
425/// <Tooltip target={btn_ref}>
426/// {"Tooltip for button."}
427/// </Tooltip>
428/// </>
429/// }
430/// }
431/// ```
432///
433/// [0]: https://getbootstrap.com/docs/5.3/components/tooltips/
434/// [2]: https://github.com/react-bootstrap/react-bootstrap/blob/master/src/Tooltip.tsx
435/// [`children`]: TooltipProps::children
436/// [`disabled`]: TooltipProps::disabled
437/// [portal]: https://yew.rs/docs/advanced-topics/portals
438/// [`popper-rs`]: https://github.com/ctron/popper-rs/
439/// [`show`]: TooltipProps::show
440/// [`target`]: TooltipProps::target
441/// [trigger_on_focus]: TooltipProps::trigger_on_focus
442/// [trigger_on_hover]: TooltipProps::trigger_on_hover
443/// [wrapper element]: https://getbootstrap.com/docs/5.3/components/tooltips/#disabled-elements
444#[function_component]
445pub fn Tooltip(props: &TooltipProps) -> Html {
446 let tooltip_ref = use_node_ref();
447
448 // Adapted from https://github.com/ctron/popper-rs/blob/main/examples/yew/src/example/basic.rs
449 let options = use_memo(props.placement, |placement| Options {
450 placement: *placement,
451 modifiers: vec![Modifier::Offset(Offset {
452 skidding: 0,
453 distance: 6,
454 })],
455 strategy: Strategy::Fixed,
456 ..Default::default()
457 });
458
459 let popper = use_popper(props.target.clone(), tooltip_ref.clone(), options).unwrap();
460
461 let focused = use_state_eq(|| false);
462 let focus_should_trigger = use_state_eq(|| props.trigger_on_focus.should_trigger());
463 let hovered = use_state_eq(|| false);
464
465 let onshow = {
466 let focused = focused.clone();
467 let hovered = hovered.clone();
468 Callback::from(move |evt_type: String| match evt_type.as_str() {
469 "mouseenter" => hovered.set(true),
470 "focusin" => focused.set(true),
471 _ => {}
472 })
473 };
474
475 let onhide = {
476 let focused = focused.clone();
477 let hovered = hovered.clone();
478 Callback::from(move |evt_type: String| match evt_type.as_str() {
479 "mouseleave" => hovered.set(false),
480 "focusout" => focused.set(false),
481 _ => {}
482 })
483 };
484
485 let focus_should_trigger_listener = {
486 let focus_should_trigger = focus_should_trigger.clone();
487
488 Callback::from(move |v: bool| {
489 focus_should_trigger.set(v);
490 })
491 };
492
493 use_effect_with(props.trigger_on_focus, |trigger_on_focus| {
494 let r = if let Some(media_query_list) = trigger_on_focus.media_queries() {
495 let media_query_list_listener = Closure::<dyn Fn(MediaQueryListEvent)>::wrap(Box::new(
496 move |e: MediaQueryListEvent| {
497 focus_should_trigger_listener.emit(e.matches());
498 },
499 ));
500
501 let _ = media_query_list.add_event_listener_with_callback(
502 "change",
503 media_query_list_listener.as_ref().unchecked_ref(),
504 );
505
506 Some((media_query_list_listener, media_query_list))
507 } else {
508 // Current trigger_on_focus rule doesn't need a MediaQueryList change event listener.
509 None
510 };
511
512 move || {
513 if let Some((media_query_list_listener, media_query_list)) = r {
514 let _ = media_query_list.remove_event_listener_with_callback(
515 "change",
516 media_query_list_listener.as_ref().unchecked_ref(),
517 );
518
519 drop(media_query_list_listener);
520 }
521 }
522 });
523
524 if props.disabled {
525 // Whenever this component is disabled, explicitly set our focus and
526 // hover state to false.
527 focused.set(false);
528 hovered.set(false);
529 }
530
531 let show = !props.disabled
532 && (props.show
533 || (*focused && *focus_should_trigger)
534 || (*hovered && props.trigger_on_hover));
535 let data_show = show.then(AttrValue::default);
536
537 use_effect_with((show, popper.instance.clone()), |(show, popper)| {
538 if *show {
539 let popper = popper.clone();
540
541 spawn_local(async move {
542 popper.update().await;
543 });
544 }
545 });
546
547 use_effect_with(
548 (tooltip_ref.clone(), popper.state.attributes.popper.clone()),
549 |(tooltip_ref, attributes)| {
550 tooltip_ref.apply_attributes(attributes);
551 },
552 );
553
554 // Attach event handlers. These are always wired up, just we ignore the
555 // result when they're disabled.
556 use_effect_with(props.target.clone(), |target_ref| {
557 let show_listener = Closure::<dyn Fn(Event)>::wrap(Box::new(move |e: Event| {
558 onshow.emit(e.type_());
559 }));
560 let hide_listener = Closure::<dyn Fn(Event)>::wrap(Box::new(move |e: Event| {
561 onhide.emit(e.type_());
562 }));
563 let target_elem = target_ref.cast::<HtmlElement>();
564
565 if let Some(target_elem) = &target_elem {
566 let _ = target_elem.add_event_listener_with_callback(
567 "focusin",
568 show_listener.as_ref().unchecked_ref(),
569 );
570 let _ = target_elem.add_event_listener_with_callback(
571 "focusout",
572 hide_listener.as_ref().unchecked_ref(),
573 );
574
575 let _ = target_elem.add_event_listener_with_callback(
576 "mouseenter",
577 show_listener.as_ref().unchecked_ref(),
578 );
579 let _ = target_elem.add_event_listener_with_callback(
580 "mouseleave",
581 hide_listener.as_ref().unchecked_ref(),
582 );
583 };
584
585 move || {
586 if let Some(target_elem) = target_elem {
587 let _ = target_elem.remove_event_listener_with_callback(
588 "focusin",
589 show_listener.as_ref().unchecked_ref(),
590 );
591 let _ = target_elem.remove_event_listener_with_callback(
592 "focusout",
593 hide_listener.as_ref().unchecked_ref(),
594 );
595 let _ = target_elem.remove_event_listener_with_callback(
596 "mouseenter",
597 show_listener.as_ref().unchecked_ref(),
598 );
599 let _ = target_elem.remove_event_listener_with_callback(
600 "mouseleave",
601 hide_listener.as_ref().unchecked_ref(),
602 );
603 }
604 drop(show_listener);
605 drop(hide_listener);
606 }
607 });
608
609 use_effect_with(
610 (props.target.clone(), props.id.clone(), show),
611 |(target_ref, tooltip_id, show)| {
612 let Some(target_elem) = target_ref.cast::<HtmlElement>() else {
613 return;
614 };
615
616 match (tooltip_id, show) {
617 (Some(tooltip_id), true) => {
618 let _ = target_elem.set_attribute("aria-describedby", tooltip_id);
619 }
620 _ => {
621 let _ = target_elem.remove_attribute("aria-describedby");
622 }
623 }
624 },
625 );
626
627 let mut class = classes!["tooltip", "bs-tooltip-auto"];
628 if props.fade {
629 class.push("fade");
630 }
631 if show {
632 class.push("show");
633 }
634
635 let mut popper_style = popper.state.styles.popper.clone();
636 // Make sure `<Tooltip>` doesn't interfere with events going to other
637 // elements, even when hidden.
638 popper_style.insert("pointer-events".to_string(), "none".to_string());
639
640 create_portal(
641 html_nested! {
642 <div
643 ref={&tooltip_ref}
644 role="tooltip"
645 {class}
646 style={&popper_style}
647 data-show={&data_show}
648 id={props.id.clone()}
649 >
650 <div
651 class="tooltip-arrow"
652 data-popper-arrow="true"
653 style={&popper.state.styles.arrow}
654 />
655 <div class="tooltip-inner">
656 { for props.children.iter() }
657 </div>
658 </div>
659 },
660 gloo_utils::body().into(),
661 )
662}